Errors and limits
What an error looks like, what each code means and what to do about it, and how the API tells you to slow down.
The error format
Every error has success: false, a stable code to branch on, a message for people and, when useful, details. Validation errors list each bad field under details.errors.
Every response carries an X-Request-Id. Log it, and include it when you contact support.
Retry only what can succeed on a retry: a 429 after Retry-After, and a 5xx with backoff. For a request that creates something, send an Idempotency-Key where the endpoint takes one, so a retry never acts twice.
HTTP/1.1 400 Bad Request
X-Request-Id: 3f1c9a2e-8b4d-4f6a-9c2e-1d7b5a3e9f20
{
"success": false,
"error": {
"code": "validation_error",
"message": "Invalid request body",
"details": {
"errors": {
"instructions": ["Invalid input: expected string, received undefined"]
}
}
}
}Error codes
| HTTP | Code | Meaning | What to do |
|---|---|---|---|
| 401 | unauthorized | The Authorization header is missing or is not Bearer sk_…. | Send the key as a Bearer token. |
| 401 | invalid_api_key | The key is unknown, revoked, or its secret is wrong. | Check the key in Settings → API keys; create a new one if it was revoked. |
| 401 | api_key_expired | The key passed its expiry date. | Create a new key in Settings → API keys. |
| 403 | forbidden | The key lacks the scope this endpoint needs (details.required_scope, details.granted_scopes). | Add the scope to the key in Settings → API keys. |
| 403 | ip_not_allowed | The key can't be used from this IP address. | Add the address to the key's allowed list in Settings → API keys. |
| 400 | validation_error | A field or parameter is missing or invalid (details.errors per field). | Fix the request; retrying it unchanged fails the same way. |
| 409 | validation_error | The request conflicts with current state: a duplicate (contact phone, goal title, enrollment), a campaign that is not active, history that blocks a change. | Read the message and details; change the state or the request. |
| 404 | not_found | No such resource in your workspace (an id of another workspace answers the same). | List the resource to find its id. |
| 429 | rate_limited | The workspace used its requests for the window (on POST /v1/calls: no free call slot). | Wait Retry-After seconds, then retry. |
| 402 | insufficient_credits | POST /v1/calls: the workspace has no credits for the call. | Top up credits in Billing. |
| 409 | no_agent_deployed | POST /v1/calls: no deployed agent on the channel. | Deploy an agent on the channel. |
| 409 | do_not_contact | POST /v1/calls: the person opted out or the number is on the exclusion list. Nothing was dialed. | Skip the number; do not retry it. |
| 409 | IDEMPOTENCY_CONFLICT | Another request with the same Idempotency-Key is still running. | Retry shortly with the same key. |
| 503 | IDEMPOTENCY_UNAVAILABLE | The Idempotency-Key could not be recorded, so nothing was sent. | Wait Retry-After seconds, then retry with the same key. |
| 500 | internal_error | The request failed on our side (502 or 503 while a release is being deployed). | Retry with backoff; contact support with the X-Request-Id. |
Each endpoint's page lists the errors specific to it. A 501 from POST /v1/calls/:id/terminate means that call's carrier can't be hung up through the API.
Rate limits
Limits count per workspace, across all of its API keys. MCP requests count in their own bucket with the same limits, and each tool call also counts like the REST request it runs. Calls have their own limits from your plan: POST /v1/calls queues a call until a line is free. More on rate limits.
| Header | What it says |
|---|---|
RateLimit-Limit | Requests allowed in the hourly window. |
RateLimit-Remaining | Requests left in the window. |
RateLimit-Reset | When the hourly window resets, as a Unix timestamp (seconds). |
Retry-After | On a 429 only: seconds to wait, counting the per-minute window too. Retrying sooner fails the same way. |
// Retry a 429 after the time the API asks for, and a 5xx with backoff
async function nutalk(path, init = {}) {
for (let attempt = 1; attempt <= 4; attempt++) {
const response = await fetch(`https://www.nutalk.ai/v1${path}`, {
...init,
headers: { Authorization: `Bearer ${process.env.NUTALK_API_KEY}`, ...init.headers },
});
const retryable = response.status === 429 || response.status >= 500;
if (!retryable || attempt === 4) return response;
const retryAfter = Number(response.headers.get("Retry-After"));
const wait = retryAfter > 0 ? retryAfter * 1000 : 2 ** attempt * 500;
await new Promise((resolve) => setTimeout(resolve, wait));
}
}