Errors
Every non-2xx response is an RFC 9457 application/problem+json body:
{ "type": "https://docs.vocapable.com/errors/contact_not_dialable", "title": "Contact blocked by scrub", "status": 409, "detail": "Contact cont_01JA0M… is on the federal DNC list (dataset 2026-07-27).", "instance": "/v1/campaigns/camp_01JA0V.../launch", "errors": [{ "field": "contact_id", "reason": "dnc_federal" }]}typeis stable and machine-matchable, and it resolves: every code the API can emit has a page athttps://docs.vocapable.com/errors/{code}, indexed in the error registry.errors[]carries field-level detail for validation failures.invalid_requestis the only code that routinely carries many entries.detailis human-oriented prose. Log it; never branch on it.
Branch on the code, not the status
Section titled “Branch on the code, not the status”The final path segment of type is the error code (snake_case), and the
registry is a promise: a code is never re-pointed at a different meaning
A changed meaning is a new code, because the old one is something an
integrator wrote an if against. Branch on the code alone and treat the
status as redundant confirmation:
const problem = await response.json();const code = new URL(problem.type).pathname.split("/").pop();
switch (code) { case "scrub_unacknowledged": // fresh report exists: acknowledge it, then relaunch break; case "rate_limited": // honor Retry-After break;}The two families of 5xx
Section titled “The two families of 5xx”Neither is ever the caller’s fault, and neither is fixed by changing the request:
503means this deployment is missing a dependency it needs (the database, a live telephony provider, the calendar aggregator). The request may succeed later or on another environment. Retry with backoff.501means the code path is deliberately unbuilt here, and retrying will never help until it ships (for exampleknowledge_upload_extractor_unavailable). Surface it; do not retry.
Codes worth special handling
Section titled “Codes worth special handling”unauthenticated(401) is byte-identical for absent, malformed, unrecognized, revoked, and expired keys, so you cannot distinguish them, by design. Rotate the credential.rate_limited(429) arrives withRetry-Afterand theRateLimit-*headers; see Conventions.contact_not_dialable(409) carriesblocked_reasons[]inerrors[]. When it is raised by a compliance-floor control it is never waivable, so treat it as terminal for that contact, not as a retry candidate.- The idempotency trio,
idempotency_key_required,idempotency_key_reuse,idempotency_key_in_flight, is covered in Conventions.
Validation errors are 422, structured
Section titled “Validation errors are 422, structured”Body or query contract failures return 422
invalid_request, with one errors[] entry per
offending field, as in [{"field": "schedule.start_date", "reason": "…"}]. Bad
filters and cursors are 400 (invalid_filter,
invalid_cursor). Wire your client to render
errors[] next to the fields it names; the platform will never return a
bare 500 for a validation problem.
The full registry, listing every code, its status, and its meaning, grouped exactly as the API documentation registers them, lives at /errors.