Tone docs

Compliance as an API

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

curl "$TONE_API/v1/compliance/check" \
  -X POST \
  -H "Authorization: Bearer $TONE_KEY" \
  -H 'content-type: application/json' \
  -d '{"e164":"+919876543210"}'
{
  "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

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.

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.

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:

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:

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

# 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:

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:

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) or rent a number and bring your own voice stack.

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.

Was this page helpful?

On this page