Skip to content
RelationsAI
Resources

Troubleshooting

Start here when something does not work as expected. Each section describes a symptom, how to confirm the cause and how to fix it. For a specific error code, open its page from the error-code reference.

Three habits make every problem faster to solve:

  1. Read error.code, not the HTTP status alone and never the message text. Each code links to its own page through error.doc_url.
  2. Read error.retry: no means retrying as is cannot help.
  3. Keep error.request_id (also the X-Request-Id header). Support needs it.

When a key or workspace problem is suspected, ask the API who you are. This request returns the key's workspace, mode and scopes, or the exact error with its fix link:

curl -X GET https://voice-api.relationsai.com/v1/workspace \ -H "Authorization: Bearer <token>"

200 shows the workspace, the key's id, mode and current scopes. An error shows its error.code, error.request_id and the link to its page in error.doc_url.

Complete runnable example

First step for any 401 or 403: ask the API which workspace, mode and scopes your key has.

The same steps as a complete script, with error handling, tested against test mode on every docs build. Each script reads your test key from an environment variable named in its first lines.

Requests fail with 401

The key is missing, malformed or no longer valid. The code tells you which:

  • authentication_required: No Authorization header, or not in the form Bearer <key>. Send Authorization: Bearer rai_test_... (one space, no quotes).
  • invalid_api_key: The key does not exist: a typo, a truncated copy, extra whitespace or a newline from a file. Copy the key again; trim whitespace when reading it from a file or variable.
  • api_key_expired: The key passed its expiry date. An admin creates a new key; plan rotations before expiry.
  • api_key_revoked: An admin revoked the key. Use the replacement key.
  • legacy_api_key_not_supported: An older sk_live_... key. Create a scoped rai_live_... or rai_test_... key in the console.

Check the environment your program really runs with: a stale variable in a shell, container or CI secret is the most common cause.

Requests fail with 403

The key is valid but not allowed to do this. Five different causes:

  • insufficient_scope: The key was created without a scope this operation needs; details.required_scopes lists them. An admin creates a key that includes those scopes.
  • insufficient_permissions: The scope is on the key, but the member who created it lost that permission (role change, left the workspace). An admin creates a new key from an account that has the permission.
  • api_access_not_enabled: The workspace is not enabled for the API (private preview). Write to support@relationsai.com.
  • ip_not_allowed: The workspace IP allowlist does not include your server. An admin adds your egress address in the console security settings.
  • live_key_not_allowed_in_browser: A live key sent with an Origin header, which browsers add. Call the API from your server; use a test key for browser experiments.

Also 403, but about the account or a feature rather than the key: account_inactive (trial ended or account suspended; reads still work) and webhooks_not_enabled (webhook delivery is not available yet; check capabilities.webhooks).

An id returns 404

resource_not_found means "no such object for this key". The API answers the same way for an id that does not exist, an id from the other mode, an id from another workspace and a malformed id, so as not to reveal anything. Check, in order:

  1. The mode. Test and live data are separate: a call, contact, campaign or webhook endpoint created with a test key does not exist for a live key, and the reverse. Only agents and phone numbers are shared (and the Sample agent and sample number exist only in test mode).
  2. The workspace. Is this key from the same workspace that created the object?
  3. The id itself. Copy it whole, with its prefix (call_, cmp_, ct_...). Ids are opaque: do not build or shorten them.

route_not_found means the path does not exist: usually a base URL without /v1, a typo in the path, or a path from another API. method_not_allowed (405) means the path exists with another method.

Requests fail with 400 or 422 validation errors

  • validation_error (400): A field is missing, unknown, of the wrong type or out of range. Bodies are strict: unknown fields are refused. details.errors lists each problem with field, issue and message. Fix each listed field.
  • invalid_phone_number (400): Not E.164. +, country code, number, no spaces or punctuation: +919000000001.
  • invalid_json (400): The body is not valid JSON. Serialise with your JSON library, do not build strings by hand.
  • unsupported_media_type (415): Missing or wrong Content-Type. Send Content-Type: application/json.
  • invalid_idempotency_key (400): You sent an Idempotency-Key (optional) with spaces, non-ASCII characters or is too long. 1 to 255 printable ASCII characters; a UUID is ideal.
  • invalid_cursor (400): A cursor was changed, or reused with different filters. Send next_cursor unchanged, with the same filters.
  • payload_too_large (413): The body is larger than 1 MB. Split the request. (Too many items in a list, for example more than 1000 recipients, is a 400 validation_error instead.).
  • custom_field_not_found (422): A custom field name is not defined in the console. Define it, or fix the name.

Conflicts: 409, 412 and 428

Idempotency conflicts happen only when you send the optional Idempotency-Key and it was already used:

  • idempotency_key_reused: the same key with a different request. You probably reused a key for a new action, or the body changed between retries (a timestamp in the body, a re-ordered list). Use a new key for a new action; keep retry bodies identical.
  • idempotency_key_in_progress: the first attempt is still running. Wait Retry-After, then resend the same request with the same key.
  • idempotency_outcome_unknown: the first attempt was interrupted and may or may not have taken effect. Check the state before acting (Retry and reliability practices).

State conflicts mean the object is not in a state that allows the action: campaign_state_conflict (read the campaign's capabilities), campaign_operation_in_progress (another start or resume is running), contact_already_exists (use upsert), contact_on_dnc_list, webhook_endpoint_disabled, webhook_delivery_not_retryable.

Concurrency: precondition_required (428) means a PATCH without If-Match; version_conflict (412) means the object changed since you read it. Read it again, reapply your change, and send the new ETag (Concurrency).

The agent is not ready

agent_not_ready lists every reason in details.reasons. Call GET/agents/{agent_id}/readiness for the same list with explanations, and fix them in the console: switch the agent on, publish it, assign an outbound number (Creating and preparing agents).

Related refusals about calling setup: phone_number_required (the agent has several outbound numbers: send phone_number_id), phone_number_not_usable (that number cannot call out for this agent), calling_not_configured (details.reason names the missing setup step), telephony_disconnected (reconnect the telephony account in the console).

Calls are refused

A refused call was not placed and not charged. The code says which rule applied:

  • dnc_listed: The number is on the do-not-call list. Do not call it.
  • consent_required: Consent is required and the contact has none. Record consent only if it was given.
  • outside_calling_hours: Outside the workspace calling window (details.window, details.timezone) Retry inside the window; the timezone is the workspace's, not the contact's.
  • contact_daily_limit_reached: Too many calls to this number in 24 hours. Try later.
  • call_not_accepted: The telephony network definitively refused the call. Check the number; contact support if valid numbers keep being refused.

See Consent and DNC. Never route around a refusal.

Balance and limits

  • insufficient_balance (402): The balance cannot cover the call or campaign; details shows both amounts. An admin tops up in the console.
  • spend_limit_reached (402): The monthly spend limit set in the console is reached. An admin raises it.
  • campaign_budget_reached (402): The campaign reached its budget_cap (spend can pass the cap by up to about 15 minutes of calls before the pause). Raise or clear the cap with PATCH, then start or resume.
  • daily_call_limit_reached (429): The workspace's daily limit of calls placed through the API (live mode, resets 00:00 UTC) Wait for the reset; check for loops; ask support if the volume is expected.
  • calling_capacity_reached (429): Too many call starts right now; nothing was dialled. Wait Retry-After and send it again; use a campaign for volume.
  • contact_limit_reached (409): The contact store is at its current capacity. Delete contacts you no longer need, or contact support.

A campaign did not resume

  • campaign_recipients_blocked (409): Remaining recipients are now blocked (do-not-call, consent, daily limit); details lists them. Do not stop and start it (a new start calls every previous recipient again, including people already reached). List the recipients with status=pending, create a new campaign with them minus the blocked ones, and stop this one.
  • campaign_not_resumable (409): The paused run can no longer continue. To reach only the people not called yet, create a new campaign with the pending recipients (a new start of this campaign calls everyone again).
  • campaign_recipients_unavailable (503): Recipients could not be screened right now; nothing was dialled. Wait Retry-After and send it again; if it keeps failing on a very large campaign, create a new campaign with the pending recipients.
  • campaign_outcome_unknown (409): A start or resume was sent and not confirmed; dialling may have begun. Wait, then read the campaign; send the action again only if it did not start (with a new key if you use idempotency keys).
  • campaign_dnc_override_enabled (409): Do-not-call screening is switched off for this campaign in the console, so the API refuses it. An admin switches it back on, or starts it from the console.
  • campaign_state_conflict (409): The campaign is not paused. Read its status and capabilities.

If a campaign paused by itself, the campaign.paused event's data.reason says why (budget, balance, spend limit, telephony, failure streak). Fix that first. A pause that answered campaign_pause_not_confirmed may still be dialling: read the campaign, then pause again or stop the campaign if it is still running.

An artifact is not ready

The transcript, recording or analysis answers 200 with state: "pending": the call has not settled. Wait for call.settled. state: "absent" means there is none (the call did not connect, or nobody spoke). A recording link that answers media_link_expired (410) passed its expires_at (about 15 minutes): read the recording again for a fresh link and download it straight away. A link that answers 404 before its expires_at belongs to a key that was revoked, expired, lost recordings:read or whose creator left the workspace: get a new link with a valid key. See Retrieving recordings and transcripts.

Webhooks do not arrive or fail verification

Work through these in order:

  1. Is delivery enabled? capabilities.webhooks on GET/workspace must be true.
  2. Right mode, events and status? Test-mode events go only to endpoints created with a test key, live to live. Check the endpoint's livemode, events and status: enabled. Events that occur while an endpoint is disabled are not delivered later.
  3. What does the delivery log say? GET/webhook-endpoints/{webhook_endpoint_id}/deliveries: error is timeout (answer within 10 seconds), connection_failed, tls_error (check your certificate chain), blocked_destination (the URL resolves to a non-public address) or http_error with your server's response_status.
  4. Send a test event (POST/webhook-endpoints/{webhook_endpoint_id}/test) and watch your server's logs.
  5. Signature mismatch (your server answers 400): verify the raw body before parsing, use the whole whsec_... secret as the key, accept any of several v1 values, and check your clock is within 5 minutes. Check secret_last4 against the secret you deployed. The verifier's command-line mode checks a saved delivery (Verifying webhook signatures).
  6. Late or bursty deliveries: after 5 failed attempts in a row an endpoint is paused (1 minute, doubling up to 1 hour), at most 2 attempts run at once per endpoint, and test-mode deliveries are limited to 60 a minute per workspace. The backlog drains on its own once your endpoint answers 2xx again; paused time does not use up attempts.
  7. Lost signing secret: a create or rotate retried with the same optional idempotency key returns secret: null. Rotate again and deploy that secret (Verifying webhook signatures).
  8. Redirects are not followed: register the final HTTPS URL. Frameworks that require authentication or CSRF tokens on every POST must exempt the webhook path.

After fixing, redeliver failed deliveries with POST/webhook-deliveries/{webhook_delivery_id}/retry, and run a reconciliation for anything older.

Requests fail with 429

rate_limited means a rate limit for the key or the workspace was exceeded in the operation's class (read, write or dial). Wait the seconds in Retry-After, then slow down: read RateLimit-Remaining and RateLimit-Reset on every response, pace batch jobs, give each component its own key, and use webhooks and list requests instead of polling. Other keys of the same workspace share the workspace limit. The 429 codes daily_call_limit_reached and calling_capacity_reached are not rate limits; see Balance and limits.

Requests that are refused before they reach an operation (an invalid or missing key, an unknown route, a wrong method, a malformed body, an IP that is not allowed) are also limited per IP address, about 300 per minute. A script stuck on 401s can therefore get 429 rate_limited for every request from that machine, even with a valid key. Fix the underlying error first, and stop retrying 4xx answers in a loop.

Requests fail with 5xx

  • service_unavailable (503): a dependency is unavailable and the request was refused before it ran. The API fails closed: it never skips a check because something is down. Send it again after Retry-After.
  • internal_error (500): an unexpected fault. For a write, it may have been applied, and a retry (with or without an idempotency key) runs the request again. Read the resource first (for a call, list calls by to and created_after and match your metadata); retry with the same key only if it did not happen.
  • If either persists for more than a few minutes, write to support@relationsai.com with the request ids.

Getting help

Email support@relationsai.com with:

  • the request_id of one or more failing requests (from the error body or the X-Request-Id header),
  • the time in UTC and the operation (method and path),
  • the mode (test or live) and the key id (key_..., from GET/workspace; never the key itself),
  • what you expected and what happened.

Never send API keys or full Authorization headers, webhook signing secrets, recording links, customer personal data (names, phone numbers, contact details) or transcripts, recordings and analysis of real calls; send ids (key_..., call_...) instead. See Support.