Department 03
The Suppression API lets you check an identifier against registered suppressions — but it never accepts an identifier in plain text. You calculate a hash yourself, with your own pepper, and send only that. We never receive an email address or a phone number.
sha256:<your own pepper + the value> locally, before anything crosses TLS. The shape is strict: sha256: followed by exactly 64 hexadecimal characters. A raw value like anna@example.se isn't even a valid attempt at an identifier_hash — it doesn't match the format and is rejected with 400.mm_live_… in production, mm_test_… against staging — not the same Bearer token as the rest of the API. Wrong environment, an unknown key, or no key at all give the same 401 in all three cases; nothing leaks about which keys exist.curl -sS https://api.marketmute.com/v1/suppression/check \
-H "Authorization: Bearer mm_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: <example-idempotency-key>" \
-d '{
"channel": "email",
"purpose": "direct_marketing",
"identifier_hash": "sha256:9f1a3c00e2b7d4f1a0c5e6b7d8f9a0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7",
"sender_ref": "kampanj-2026-h2"
}'
{
"error": "tokenization_not_provisioned",
"message": "Spärrkontrollen kan inte besvaras för den här identifieraren. ...",
"receipt_id": "sup_rcpt_...",
"checked_at": "2026-08-24T10:15:00Z"
}
POST /v1/suppression/batch takes the same channel/purpose plus a list of identifier_hashes. The cap is 10,000 hashes per call — a batch over the cap is rejected in FULL with 400 batch_too_large; nothing is silently truncated for you.The suppressions in the register are stored as MarketMute-calculated HMAC tokens over the normalized value. A hash YOU calculated with your own pepper can't be compared against them, and the raw value that would be needed to recalculate deliberately isn't kept on our side — that's the whole point of never receiving plain text. A per-sender index for that comparison is therefore missing from the data model.
The call is validated, booked against your quota, and logged as usual. What's missing is exactly the matching outcome. We'd rather respond 501 with an explanation than 200 with a made-up value — the same principle the rest of the API follows whenever a surface isn't live yet.
Nine possible responses, one at a time — expand the code you actually got to read what it means and what you do next.
tokenization_not_provisionedAlways this, for a structurally valid call today.
Read message and receipt_id. Don't wait for a different answer — watch the changelog instead for when matching goes live.
invalid_bodyThe body is missing or can't be parsed as JSON.
Fix the call.
invalid_identifier_hashA value doesn't match sha256:<64 hex> — including raw identifiers.
Calculate the hash locally before you send.
empty_batchidentifier_hashes is missing or empty (/batch only).
Send at least one hash.
batch_too_largeMore than 10,000 hashes in one batch. The whole call is rejected.
Split it across multiple calls.
unauthorizedThe key is missing, unknown, or belongs to the wrong environment.
Check the Authorization header and the mm_test_/mm_live_ prefix.
idempotency_conflictIdempotency-Key reused with a DIFFERENT body.
Use a new key per unique request.
unsupported_scopeUnknown channel or purpose.
See valid values below.
rate_limitedThe key's quota is used up.
Wait the number of seconds Retry-After specifies.
| Field | Values |
|---|---|
| channel | phone, email, sms, direct_mail, profiling, crm_marketing, all_direct_marketing |
| purpose | direct_marketing, profiling |
The quota is counted per API key, not per IP — otherwise a single 10,000-hash batch could dodge the call cap. A response always carries the RateLimit-Remaining header; a 429 response also carries Retry-After in seconds.
| Limit | Default | On exceeding |
|---|---|---|
| Calls / minute | 1,000 | 429 |
| Hashes / minute | 100,000 | 429 |
How the suppression register stays separate from personal data is covered in department 05. Register changes may in future be delivered as signed events — see department 04.