Skip to main content

Message Suppression API

Configure content-hash duplicate suppression: a send is silently skipped when the same message body already reached the same contact on the same channel inside a window you choose. This is the “two different campaigns fired the identical promo back-to-back” guard — distinct from frequency caps (which count send firings per contact) and opt-outs (which block a channel entirely). Enforcement happens inside the message send pipeline, before frequency capping, so a duplicate never burns a cap slot. A suppressed send reports as skipped (reason duplicate_content) rather than an error, so campaigns and batches keep moving. GET: returns the org’s single policy or null when none is set.
DELETE: turns the policy off (the operation is idempotent — a second DELETE returns the same 204).
Base path: /api/v1/message-suppression Authentication: API key (X-API-Key) or session JWT. Reads require any role; the upsert and delete writes require owner, admin, or developer.

Using the SDKs

Python (same call via the SDK’s escape hatch):
The Python SDK is core-scope — it wraps the 8 core resources (messaging, voice, contacts, campaigns, verify, numbers) and reaches everything else through the generic client.request() escape hatch above. See the Python SDK. Returns the typed ApiResponse envelope. See the SDK index at SDK quickstart.

Policy shape

A single opt-in policy per organization.

Example — dedupe marketing sends within 7 days

200 OK — the configured policy (consistent with GET)
Restricting applies_to_categories to marketing means OTP and transactional resends are never suppressed. Omit channels (or send null) to cover every messaging channel.
Suppression fails open: if no policy applies, or the backing store is briefly unavailable, the send is dispatched normally — a transient blip never silently swallows outbound traffic. Bodies are compared by SHA-256 of the whitespace-normalized text, so cosmetically-identical copy (reflowed spacing) collapses to the same marker while different case or wording does not.

Recipes — cookbook-style

Recipe 1: same body to the same recipient, twice inside the window

A marketing campaign fires; a rerun from a different campaign name, journey step, or operator action replays the same body. Suppression catches the second send before it consumes a slot.
200 OK — first send accepted
Re-send the same payload within the 7-day window and the pipeline returns a soft skip rather than a hard error — the batch keeps moving.
Your campaign batch exporter, webhook workers, and journey enqueues see status: "skipped" with either reason: "duplicate_content" (suppression) or reason: "frequency_capped" (inter-campaign cap skip, campaign sending) and can drop the row from the “true delivery” count.

Recipe 2: any channel blocked by opt-in (still respected)

Suppression is parallel to the opt-out gate, not instead of it. A duplicate_content skip only happens when the contact is otherwise eligible; if they’ve opted out of the channel, the compliance gate blocks the send outright (no skipped reason for compliance — the send is rejected at the front).
400 — opted out (suppression never runs if the opt-out gate rejects)
Restricting applies_to_categories: ["marketing"] in your PUT body keeps transactional re-sends (password resets, receipts, OTPs) outside suppression’s scope while marketing traffic is still deduped.

Recipe 3: bulk on-call import + poll-and-verify

For bulk suppression-file imports (an on-call policy download that should become organization-wide rule), do the write through POST /api/v1/contacts/optouts/bulk — suppression’s PUT is for dedupe policy; opt-outs are the per-contact block list. Pair it with a poll loop on the async contact importer when the row count exceeds a few hundred.
Verify each row landed by polling GET /api/v1/contacts/imports/{id} (for async-import flows) or GET /api/v1/contacts/{id}/channel-state (synchronous verify) — sms.opted_out should be true on the response. Suppress-only-bulk has no separate endpoint; the policy is a single org-level object set via PUT. Use POST /contacts/optouts/bulk when the intent is “suppress this cohort on channel X” (per-contact block) and PUT /api/v1/message-suppression/ when the intent is “dedupe identical bodies within a window for everyone” (org policy). See the Opt-Outs API for the CSV import wizard endpoints.

Errors

See also