# Consent and suppression

Source: https://docs.usetone.ai/consent-and-dnc

> 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

```bash
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

```bash
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

```bash
# 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.

```bash
# 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

```bash
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:

```bash
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.
