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. Of the two ledgers, 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 with a Tone agent, 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.
It is also environment-scoped: a consent recorded with a tone_test_ key
counts for test calls only, and a test key lists and revokes only test consents.
A sandbox credential can never authorise a real call. Suppression is the
opposite — see below.
🔴 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
A suppression applies to both environments, whoever recorded it: an opt-out captured with a test key still protects that person from live calls, because over-suppressing is the safe mistake. A test key lists only the entries the test environment added.
# 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 not an error — a replayed webhook or a retried job cannot fail here — and it never weakens an entry that still holds: the entry that suppresses longer wins. A permanent complaint survives a later 90-day opt-out, and a later complaint upgrades a 90-day opt-out to permanent. An entry that was lifted or has expired is replaced.
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 with a Tone agent, a verbal "stop calling me" the post-call analysis
detects on such a call, and a carrier reporting the number permanently
unreachable each write an entry with the call recorded as its source. On a BYO
call Tone records no opt-out — it forwards the digit to your bot and has no
transcript — so report it with POST /v1/compliance/call-outcomes or
POST /v1/consent/opt-out.
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: on a
promotional call it overrides a dnd_register listing (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.