# 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](/fundamentals/error-codes).

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:

```bash
curl -X GET https://voice-api.relationsai.com/v1/workspace \
     -H "Authorization: Bearer <token>"
```
```js
const response = await fetch("https://voice-api.relationsai.com/v1/workspace", {
  method: "GET",
  headers: {
    "Authorization": "Bearer <token>",
  },
});
const workspace = await response.json();
```
```python

response = requests.get(
    "https://voice-api.relationsai.com/v1/workspace",
    headers={
        "Authorization": "Bearer <token>",
    },
    timeout=30,
)
workspace = response.json()
```

`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: [cURL script](/examples/resources/troubleshooting/check-key.sh) · [JavaScript](/examples/resources/troubleshooting/check-key.mjs) · [Python](/examples/resources/troubleshooting/check-key.py)

## Requests fail with 401

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

- [`authentication_required`](/errors/authentication_required): No `Authorization` header, or not in the form `Bearer <key>`. Send `Authorization: Bearer rai_test_...` (one space, no quotes).
- [`invalid_api_key`](/errors/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`](/errors/api_key_expired): The key passed its expiry date. An admin creates a new key; plan rotations before expiry.
- [`api_key_revoked`](/errors/api_key_revoked): An admin revoked the key. Use the replacement key.
- [`legacy_api_key_not_supported`](/errors/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`](/errors/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`](/errors/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`](/errors/api_access_not_enabled): The workspace is not enabled for the API (private preview). Write to [support@relationsai.com](mailto:support@relationsai.com).
- [`ip_not_allowed`](/errors/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`](/errors/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`](/errors/account_inactive) (trial ended or account suspended; reads still work) and [`webhooks_not_enabled`](/errors/webhooks_not_enabled) (webhook delivery is not available yet; check `capabilities.webhooks`).

## An id returns 404

[`resource_not_found`](/errors/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`](/errors/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`](/errors/method_not_allowed) (405) means the path exists with another method.

## Requests fail with 400 or 422 validation errors

- [`validation_error`](/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`](/errors/invalid_phone_number) (400): Not E.164. `+`, country code, number, no spaces or punctuation: `+919000000001`.
- [`invalid_json`](/errors/invalid_json) (400): The body is not valid JSON. Serialise with your JSON library, do not build strings by hand.
- [`unsupported_media_type`](/errors/unsupported_media_type) (415): Missing or wrong `Content-Type`. Send `Content-Type: application/json`.
- [`invalid_idempotency_key`](/errors/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`](/errors/invalid_cursor) (400): A cursor was changed, or reused with different filters. Send `next_cursor` unchanged, with the same filters.
- [`payload_too_large`](/errors/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`](/errors/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`](/errors/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`](/errors/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`](/errors/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](/guides/retry-and-reliability#when-the-outcome-is-unknown)).

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

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

## The agent is not ready

[`agent_not_ready`](/errors/agent_not_ready) lists every reason in `details.reasons`. Call [`GET /agents/{agent_id}/readiness`](/api/agents#check-whether-an-agent-can-place-calls) 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](/guides/creating-and-preparing-agents)).

Related refusals about calling setup: [`phone_number_required`](/errors/phone_number_required) (the agent has several outbound numbers: send `phone_number_id`), [`phone_number_not_usable`](/errors/phone_number_not_usable) (that number cannot call out for this agent), [`calling_not_configured`](/errors/calling_not_configured) (`details.reason` names the missing setup step), [`telephony_disconnected`](/errors/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`](/errors/dnc_listed): The number is on the do-not-call list. Do not call it.
- [`consent_required`](/errors/consent_required): Consent is required and the contact has none. Record consent only if it was given.
- [`outside_calling_hours`](/errors/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`](/errors/contact_daily_limit_reached): Too many calls to this number in 24 hours. Try later.
- [`call_not_accepted`](/errors/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](/guides/consent-and-dnc). Never route around a refusal.

## Balance and limits

- [`insufficient_balance`](/errors/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`](/errors/spend_limit_reached) (402): The monthly spend limit set in the console is reached. An admin raises it.
- [`campaign_budget_reached`](/errors/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`](/errors/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`](/errors/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`](/errors/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`](/errors/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`](/errors/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`](/errors/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`](/errors/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`](/errors/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`](/errors/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`](/errors/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`](/errors/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](/guides/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`](/api/workspace#retrieve-the-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`](/api/webhook-endpoints#list-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`](/api/webhook-endpoints#send-a-test-event)) 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](/guides/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](/guides/verifying-webhook-signatures#during-secret-rotation)).
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`](/api/webhook-endpoints#retry-a-delivery), and run a reconciliation for anything older.

## Requests fail with 429

[`rate_limited`](/errors/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`](/errors/daily_call_limit_reached) and [`calling_capacity_reached`](/errors/calling_capacity_reached) are not rate limits; see [Balance and limits](#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 `401`s 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`](/errors/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`](/errors/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](mailto:support@relationsai.com) with the request ids.

## Getting help

Email [support@relationsai.com](mailto: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`](/api/workspace#retrieve-the-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](/resources/support).
