Skip to content
RelationsAI
API fundamentals

Error handling

Every error response uses one envelope and a stable code. Handle errors by their code, use the retry hint to decide whether to try again, check whether the work may already have started, and log the request_id.

The error envelope

Every non-2xx response has this shape:

JSONJSON
{ "error": { "code": "agent_not_ready", "message": "This agent cannot place calls right now.", "details": { "agent_id": "agent_3hT9kQ2mVx7RbN4pLc8Wz1", "reasons": ["agent_not_published"] }, "request_id": "req_4k7m2qat5xvz3c6vqnwpbdfh", "retry": "no", "doc_url": "https://docs.relationsai.com/errors/agent_not_ready" } }
FieldTypeMeaning
codestringStable, machine-readable error code. Branch on this. Codes are never renamed in v1; new codes can be added.
messagestringA sentence for people, safe to show an operator. It may be reworded at any time: never parse it.
detailsobjectExtra facts for this code, such as required_scopes or errors[], documented on each code's page. Always an object; {} when the code has none.
request_idstringThe request id. Log it and quote it to support. Also log the X-Request-Id header of the response you received: for a replayed answer (Idempotent-Replayed: true), the body can carry the id of the original request.
retrystringWhat to do next: no, yes or after (below).
doc_urlstringThe code's page in the error-code reference, with causes and fixes.

Each code always comes with the same HTTP status, so you can rely on the status class (4xx, 5xx) as a fallback, but the code is what distinguishes, for example, the several kinds of 409. Error messages never include internal details, and no error is ever returned with a 2xx status.

The retry hint

retryMeaningWhat to do
noRetrying the same request will fail the same way.Change the request or the configuration first.
yesA temporary problem.Retry with exponential backoff and jitter. For a POST or DELETE, reuse the same Idempotency-Key if you sent one.
afterWait first.Wait the number of seconds in the Retry-After header, then retry (with the same Idempotency-Key, if you sent one).

"The same key" on this page means the Idempotency-Key you sent with the original request. It is optional; without one, a retried POST or DELETE runs again, so read the state before retrying a write whose outcome you do not know.

Never retry no errors in a loop. Cap retries for yes and after (for example at five attempts) and give up loudly. A very long Retry-After, such as the hours until the daily call limit resets, is a signal to schedule the work for later, not to sleep.

Did anything happen?

Most errors mean the request was refused before it did anything: nothing was created, dialled or charged. A few can happen after work may have begun, for example when the start of a call or a campaign was sent but its confirmation was lost. Never assume an error means no side effects. Each code's page states which kind it is, as Execution:

ExecutionMeaning
Not startedNothing was done. Fix the cause (or wait) and send the request again.
May have startedThe work may have happened, partly or fully. Read the resource before doing anything else; with a key, an identical retry with the same key is also safe.
Not applicableReads, where nothing is changed either way.

These codes may have started work:

  • internal_error (500, retry: yes): A 500 is not stored under an Idempotency-Key: a retry with the same key runs the request again from the start; it does not replay an earlier result. A read can simply be retried. For a write, the API does not know whether the change was applied before the error, so read the resource first (for a call, list calls filtered by to and created_after; for a campaign or a contact, retrieve it) and retry with the same key only if the change is not there.
  • idempotency_key_in_progress (409, retry: after): The first request with this key is still running. Wait Retry-After, then send the identical request with the same key to get its result.
  • idempotency_outcome_unknown (409, retry: no): The first request with this key was interrupted and its outcome is unknown. Read the resource (for a call, list calls by to and created_after). Only if it did not happen, send it again with a new key.
  • campaign_outcome_unknown (409, retry: after): A start or resume was sent but not confirmed, and dialling may have begun. This answer is stored under the key: a retry with the same key returns it again and sends nothing. Wait, then retrieve the campaign: running means it started. Only if it is not running and you still want to try, send the action with a new key; the campaign refuses a second dial of the same run.
  • campaign_pause_not_confirmed (409, retry: yes): The pause was not confirmed and dialling may continue. Retry the pause with the same key, then retrieve the campaign; if it is still running, stop it.

A timeout or a dropped connection, where you get no response at all, is the same situation: the request may or may not have run. See After a timeout.

One more case: POST /v1/calls answers 202 even when the start of the call cannot be confirmed straight away. The call is then resolved within about two minutes: it either proceeds, or ends with outcome: failed and end_reason: not_confirmed, and nothing is charged.

Handling errors by class

The error-code reference lists every code with its status, retry hint and fix.

Handling errors in code

Read the envelope once, then decide from retry and, where it matters, from code. For example, this call uses one of your own agents that is not published yet:

curl -X POST https://voice-api.relationsai.com/v1/calls \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "agent_id": "agent_3hT9kQ2mVx7RbN4pLc8Wz1", "to": "+12025550100" }'
JSONJSON
{ "error": { "code": "agent_not_ready", "message": "This agent cannot place calls right now.", "details": { "agent_id": "agent_3hT9kQ2mVx7RbN4pLc8Wz1", "reasons": [ "agent_not_published" ] }, "request_id": "req_4k7m2qat5xvz3c6vqnwpbdfh", "retry": "no", "doc_url": "https://docs.relationsai.com/errors/agent_not_ready" } }

retry is no, so sending the same request again would fail the same way. details.reasons says what to fix: publish the agent in the console, then send the request again. If you sent an Idempotency-Key, the refusal was not stored under it, so you may reuse it.

Decide the same way for every error:

  1. code is idempotency_outcome_unknown or campaign_outcome_unknown: read the resource before anything else.
  2. code is internal_error on a write: read the resource first and retry with the same key only if the change is not there. On a read, retry with backoff.
  3. retry is after: wait the number of seconds in Retry-After, then send the same request with the same key.
  4. retry is yes: retry with exponential backoff and the same key.
  5. retry is no: fix the request or the configuration (details says what), then send it again.

Complete, runnable retry helpers are on Rate limits (backoff with Retry-After) and Idempotency (retries that can never place a call twice).

When your server gets no HTTP response at all (a timeout, a reset connection or a proxy error page that is not JSON), the request may or may not have run. With an Idempotency-Key, retry with the same key, then check state if the retries run out. Without one, check state first (see Idempotency): a blind retry can repeat the action.

Log what support needs

For every failed request, log at least: the time in UTC, the method and path, the HTTP status, error.code and error.request_id. Never log the Authorization header or the key. With the request_id, support can find exactly what happened.

All error codes

See the error-code reference: every code, its HTTP status, what it means, how to fix it and whether to retry. Each code also has its own page at /errors/<code>, which is the doc_url in the error.