# Compliance as an API

Source: https://docs.usetone.ai/quickstart-compliance

> Ask "may I call this number?" and get an auditable answer — with or without placing the call through Tone.

You already have a dialer, a carrier, and a voice stack. What you don't have is a defensible answer
to "why did you call this person?" when a TRAI complaint lands.

This tier is that answer as an API: a verdict endpoint, a consent ledger, a suppression list, and an
evidence pack — all writing the **same audit rows** a call placed through Tone writes. You keep your
own infrastructure. Nothing here requires you to buy a number from us.

## 1. Declare your sender classification

The calling-window rules depend on what kind of sender you are, so say so once:

> **This one is dashboard-only.** Open **Telephony → Compliance → Sender
> classification** and set it there. An API key gets a `403`, deliberately: the
> classification decides which calling window applies to every dial you place
> on your own infrastructure, so loosening it is a decision an accountable
> person makes rather than something a deploy script does. The change is
> recorded as a `profile_change` audit row either way.

This is a **dashboard action, not an API-key one** — an API key gets a `403`. In the dashboard it's
the **Sender classification** field under **Telephony → Compliance**. Loosening the rules
that govern every subsequent call should be a deliberate act by an accountable human, and the change
is itself recorded as a `profile_change` audit row. The same applies to switching DNC enforcement
off.

## 2. Ask before you dial

```bash
curl "$TONE_API/v1/compliance/check" \
  -X POST \
  -H "Authorization: Bearer $TONE_KEY" \
  -H 'content-type: application/json' \
  -d '{"e164":"+919876543210"}'
```

```json
{
  "data": {
    "e164": "+919876543210",
    "purpose": "promotional",
    "allowed": false,
    "blockedBy": "dnc",
    "checks": [
      {
        "id": "8814",
        "checkType": "dnc",
        "outcome": "block",
        "source": "internal",
        "reason": "Opted out on 2026-06-02",
        "enforced": true,
        "detail": { "...": "..." },
        "createdAt": "2026-08-24T09:41:07.812Z"
      },
      { "id": "8815", "checkType": "time_window", "outcome": "pass", "...": "..." }
    ]
  }
}
```

`200` either way — a refusal is a verdict, not an error. Branch on `allowed`.

Three things make this useful rather than decorative:

- **`checks` is the evidence, not a summary.** Each entry is a row we persisted, with an `id` you
  can cite later. `outcome` is `pass`, `warn`, or `block`, and only `block` sets `allowed: false` —
  a carrier-DND `warn` is advisory and does not stop a call.
- **The verdict is self-describing.** It records the `purpose` it evaluated, so a verdict read back
  in six months still explains itself. Pass `purpose` explicitly to check a specific campaign's
  classification; omit it to use your declared default.
- **`blockedBy` names the check that refused**, so you can act on it — `dnc` needs a suppression
  removal, `consent` needs a consent record, `time_window` needs a different hour of the day.

### Pre-flighting a list

```bash
curl "$TONE_API/v1/compliance/check/batch" \
  -X POST \
  -H "Authorization: Bearer $TONE_KEY" \
  -H 'content-type: application/json' \
  -d '{"e164s":["+919876543210","+919812345678"],"purpose":"promotional"}'
```

Up to 100 numbers per request. Each one runs the full gate and writes its own audit rows — this is
the same work, batched, not a cheaper approximation.

## 3. Record consent

Consent legally beats preference in India: a recipient on the DND register with valid consent on
file is callable, and the record proving it is what the gate reads.

```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": "otp",
    "evidenceRef": "form-7741",
    "scope": "Offers and product updates by voice call",
    "capturedAt": "2026-08-20T11:02:00Z"
  }'
```

- **`kind`** is `explicit` (they agreed) or `inferred` (an existing relationship implies it).
- **`source`** is `api`, `web_form`, `ivr`, `otp`, `dca`, or `import`, weakest to strongest as
  evidence. `dca` mirrors the operator DLT's Digital Consent Acquisition facility.
- **`capturedAt` is when *they* consented**, not when you told us. Send it when importing history —
  otherwise every imported consent dates to the day of the import, which makes the expiry clock
  meaningless.
- **`expiresAt`** is optional for explicit consent, where policy fills it in: TCCCPR 2025 caps
  explicit transactional consent at **seven days**. It is **required** for `inferred` consent —
  that lasts exactly as long as the relationship, and only you know when that ends.

Consent is scoped to a purpose. A consent for `service` does not authorize a `promotional` call, and
the gate will tell you so.

The ledger is append-only. Revocation is a stamp, not a delete:

```bash
curl -X POST "$TONE_API/v1/consent/CONSENT_ID/revoke" -H "Authorization: Bearer $TONE_KEY"
```

## 4. Close the loop on what happened

A verdict you never acted on proves nothing. Report the outcome:

```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",
    "callReference": "your-call-id-4417",
    "notes": "Asked to be removed"
  }'
```

`outcome` is `connected`, `no_answer`, `busy`, `failed`, `opt_out`, `complaint`, or `wrong_number`.
`checkId` ties it back to the verdict you acted on; `callReference` is your own id, for your
reconciliation.

Two outcomes do real work rather than just recording:

- **`opt_out`** revokes every consent for that number and writes a 90-day suppression, in one
  transaction. Your next check on that number comes back `allowed: false`.
- **`complaint`** suppresses permanently.

This is the same function Tone's own calls use when a recipient opts out mid-conversation, so a
mixed estate — some calls through Tone, some through your own dialer — maintains one suppression
list, not two that disagree.

## 5. Manage suppression directly

```bash
# Add one
curl "$TONE_API/v1/dnc" -X POST \
  -H "Authorization: Bearer $TONE_KEY" -H 'content-type: application/json' \
  -d '{"e164":"+919876543210","source":"api","reason":"Emailed us to be removed"}'

# Import a list — up to 1000 per request
curl "$TONE_API/v1/dnc/bulk" -X POST \
  -H "Authorization: Bearer $TONE_KEY" -H 'content-type: application/json' \
  -d '{"entries":[{"e164":"+919876543210","source":"csv"},{"e164":"+919812345678","source":"csv"}]}'

# Read it back
curl -H "Authorization: Bearer $TONE_KEY" "$TONE_API/v1/dnc?limit=100"
```

This list is the only thing the gate hard-blocks on, which makes it the one to get right. Bring
your existing suppression list with you on day one.

## 6. Produce the evidence

When a complaint arrives:

```bash
curl -H "Authorization: Bearer $TONE_KEY" \
  "$TONE_API/v1/compliance/evidence?e164=%2B919876543210"
```

You get everything held about that number: every check as it was recorded at the time, every
consent including revocations, the suppression entry, and the outcomes you reported — raw evidence,
not a summary of it. `truncated` is honest if the window held more than the cap.

For a rolling view of decisions:

```bash
curl -H "Authorization: Bearer $TONE_KEY" \
  "$TONE_API/v1/compliance/checks?check_type=dnc&outcome=block&limit=50"
```

Cursor-paginated like every list endpoint.

## What this tier does and doesn't do

**It does:** run the same gate Tone's own dials run, persist every decision, keep an auditable
consent ledger, maintain your suppression list, and hand you an evidence pack per number.

**It doesn't stop your dialer.** The verdict is advisory to *you* — we have no way to prevent a call
you place on your own carrier. What we can prove is that you asked, what you were told, and what you
did next. If you want refusals actually enforced at dial time, place the calls through Tone
(see the [quickstart](/quickstart)) or rent a number and
[bring your own voice stack](/quickstart-byo).

**One case has no pre-dial hook at all:** if you dial over a SIP trunk, your platform reaches the
carrier directly and nothing of ours is in the path. The verdict API is your pre-flight, and the
carrier's CDR — which we ingest and price — is the evidence.

## Billing

Verdicts are metered per number checked, and charged **before** the gate runs — an unpaid verdict
never leaves audit rows that look like a paid one. Checks made with a `tone_test_` key are free, so
build and test your integration at no cost.
