Errors & compliance
The error envelope
Every error, on every endpoint, has the same shape:
{
"error": {
"code": "conflict",
"message": "campaign cannot transition from completed to running"
}
}code is stable and machine-readable — branch on it. message is for humans and may be reworded at any time — never parse it.
| HTTP | error.code | Meaning |
|---|---|---|
400 | bad_request | Malformed JSON, bad query params, wrong field types. |
400 | invalid | A field violates a domain rule (validation error). |
401 | unauthorized | Missing, unknown or revoked API key — see Authentication. |
403 | forbidden | Account suspended. |
404 | not_found | Unknown, malformed or another account's id — existence is never leaked. |
409 | conflict | Uniqueness or state-machine violation (duplicate name, invalid lifecycle transition, busy seat). |
415 | unsupported_media_type | Body-carrying request without a JSON content type. |
422 | unprocessable | Shape is fine, semantics are not (a broken domain rule). |
422 | compliance_blocked | The compliance engine refused the operation — see below. |
429 | throttled / sms_spend_cap_exceeded | Rate and spend brakes — see Rate limits & caps. |
500 | internal | Our fault. Retry with backoff and quote the X-Request-ID to support. |
502 | provider_error / dial_error / sbc_error | An upstream (SMS provider, switch, SIP edge) rejected the operation. |
Endpoint-specific codes (e.g. sms_provider_unconfigured) are documented per response in the endpoint reference.
422 compliance_blocked — the platform that refuses to break the law
Dialer Digital has exactly one path to a consumer's phone: a gated engine that checks consent, do-not-call status, quiet hours, contact-frequency rules (Reg F 7-in-7 and post-contact), state-level limits, reassigned-number data and your own account policies — before any provider is contacted. Your API calls go through the same gates as our campaigns and our dashboard. There is no bypass parameter, and none can be added by mistake: the engine fails closed.
When a gate refuses, you get:
{
"error": {
"code": "compliance_blocked",
"message": "the compliance engine refused this send (dnc_listed)",
"gate": "dnc_listed",
"decision_id": "2c1d0e9f-8a7b-6c5d-4e3f-2a1b0c9d8e7f"
}
}gate— which rule refused:tcpa_no_consent,quiet_hours,regf_7in7,regf_post_contact,state_limit,dnc_listed,consent_revoked,cease_and_desist,number_reassigned, ortenant_policy.decision_id— a citation into the append-only audit chain. The decision that blocked the operation is durable evidence: if a consumer or regulator ever asks "did you try to contact this person?", the answer is a verifiable record, in your favor.
A block is evidence, not an error
Nothing happened — zero provider calls, zero charges. Do not retry unchanged: the input that was refused will be refused again, and hammering a gate only adds noise to your own audit trail. Fix the underlying state (obtain consent, wait out quiet hours, honor the revocation) or move on to the next account. This is the feature you are paying for — every block is a violation that never reached a consumer, packaged for your defense at GET /v1/reports/violations-prevented.
Where blocks show up
| Surface | How a block is reported |
|---|---|
One-off SMS (POST /v1/sms/messages) | 422 compliance_blocked with gate + decision_id. |
Manual/preview dial (POST /v1/campaigns/{id}/dial) | 200 with {"status": "blocked", "reason": "<gate>"} — a valid outcome the agent should see, not a server error. |
| Campaign auto-dialing | Blocks never reach the API caller; they are recorded and aggregated at GET /v1/stats/blocked. |
Turning blocks into paperwork
GET /v1/stats/blocked— by-gate counts and a daily series, computed from the append-only evidence table.GET /v1/reports/violations-prevented— the packaged report (JSON or CSV) you can hand to a client, auditor or court.GET /v1/debts/{id}/defense-packet— the sealed, per-account litigation bundle; everydecision_idyou ever received is citable against it.
404 and multi-tenancy
Cross-account ids, malformed UUIDs and genuinely unknown ids are all the same 404 not_found. This is enforced by row-level security in the database — the API cannot tell you whether someone else's resource exists, by design.