Tonedocs

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.

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.

kindExpiry
explicitPolicy fills it — capped at 7 days for a transactional purpose, otherwise until revoked
inferredYou 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.

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.

Was this page helpful?

On this page