Consent and suppression
Two different ledgers. Only one of them stops a call — and knowing which is the difference between a compliant estate and a fine.
🔴 The consent ledger passes or warns. It never blocks. Only the suppression list blocks.
That is the single most important sentence on this page. Revoking someone's consent does not stop them being called; it records that consent ended. If someone asks not to be called, they must land on the suppression list.
Opt-out does both, in one transaction
curl "$TONE_API/v1/consent/opt-out" -X POST \
-H "Authorization: Bearer $TONE_KEY" -H 'content-type: application/json' \
-d '{"e164":"+919876543210","reason":"Asked during a call"}'This revokes every active consent for the number and suppresses it under the 90-day re-consent lockout, atomically. It is one verb rather than two calls precisely because doing half of it is the failure mode that produces complaints.
The same function runs when a caller opts out mid-call, and when you report an
opt_out outcome — so a mixed estate keeps one suppression list.
Recording consent
curl "$TONE_API/v1/consent" -X POST \
-H "Authorization: Bearer $TONE_KEY" -H 'content-type: application/json' \
-d '{"e164":"+919876543210","purpose":"promotional","kind":"explicit",
"source":"web_form","capturedAt":"2026-08-20T11:02:00Z",
"evidenceRef":"form-sub-88213"}'Consent is purpose-scoped: a record for promotional does not satisfy the
gate for collections.
🔴 capturedAt is when the RECIPIENT consented, not when you called this
endpoint. It defaults to now, which is right for a live capture and wrong for
an import — importing a back catalogue without it dates every record to the day
of the import and makes the 7-day transactional clock meaningless.
kind | Expiry |
|---|---|
explicit | Policy fills it — capped at 7 days for a transactional purpose, otherwise until revoked |
inferred | You must supply expiresAt. It lasts as long as the relationship, and only you know when that ends. Omitting it is a 422 |
Records are append-only. Revoking stamps the record; it does not delete it, because the evidence that consent once existed is as important as the fact it ended.
Suppression
# one
curl "$TONE_API/v1/dnc" -X POST \
-H "Authorization: Bearer $TONE_KEY" -H 'content-type: application/json' \
-d '{"e164":"+919876543210","source":"manual","lockout90d":true}'
# bring an existing list — up to 1000 per request, one transaction
curl "$TONE_API/v1/dnc/bulk" -X POST \
-H "Authorization: Bearer $TONE_KEY" -H 'content-type: application/json' \
-d '{"entries":[{"e164":"+919876543210"},{"e164":"+919812345678"}]}'Adding a number already on the list is a no-op, not an error — a replayed webhook or a retried job cannot fail here.
lockout90d records an opt-out that becomes contactable again on a date, which
is what TCCCPR describes. Without it, the suppression is permanent — which is
what a complaint earns.
⚠️ Removing an entry makes the number dialable again. An opt-out is a legal
instruction, not a preference: remove one only when you can show the recipient
asked to be contacted again. Lifting a suppression is a dashboard-session
action with a written reason (POST /v1/dnc/{id}/remove) — API keys can add
suppressions within seconds of an opt-out, but cannot lift one, and every
removal is recorded as a suppression_change row in the audit trail. The
entry itself survives with removedAt stamped rather than disappearing. (The
old DELETE /v1/dnc/{id} still exists and always refuses, saying so.)
Numbers can also land on the list automatically: a DTMF-9 keypress during a call, a verbal "stop calling me" the post-call analysis detects, and a carrier reporting the number permanently unreachable each write an entry with the call recorded as its source.
Recording consent, and its limits
Consent for a number inside its 90-day opt-out lockout is refused with a
409 — TCCCPR forbids re-acquiring consent during it, and accepting the
record would hand the gate a pass that contradicts the suppression. A
permanently suppressed number (a complaint) requires lifting the suppression
first.
A valid explicit consent also does one more thing at dial time: it overrides an advisory carrier-DND flag (consent legally beats the preference register). It never overrides your suppression list.
# bring an existing consent base — up to 1000 per request, accepted PER ROW
curl "$TONE_API/v1/consent/bulk" -X POST \
-H "Authorization: Bearer $TONE_KEY" -H 'content-type: application/json' \
-d '{"records":[{"e164":"+919876543210","purpose":"promotional","kind":"explicit","source":"import","capturedAt":"2026-06-01T10:00:00Z"}]}'Bulk import accepts rows individually: a record that violates policy (an
inferred consent missing expiresAt, a locked-out number) comes back in
rejected with its index and reason while the rest land. 🔴 Set capturedAt
on every imported record — it defaults to now, which dates your whole back
catalogue to the day of the import.
Closing the loop from your own dialer
curl "$TONE_API/v1/compliance/call-outcomes" -X POST \
-H "Authorization: Bearer $TONE_KEY" -H 'content-type: application/json' \
-d '{"e164":"+919876543210","outcome":"opt_out","checkId":"8814"}'opt_out and complaint are not just labels — they suppress the number through
the same path a mid-call opt-out uses. opt_out applies the 90-day lockout;
complaint suppresses permanently.
The evidence pack
When a complaint arrives, this is the answer:
curl -H "Authorization: Bearer $TONE_KEY" \
"$TONE_API/v1/compliance/evidence?e164=%2B919876543210"Every check, consent record, suppression entry and reported outcome Tone holds for that number, with timestamps.