Errors
Every failure returns the same envelope, whatever the endpoint:
{
"error": {
"type": "invalid_request_error",
"code": "AGENT_NOT_FOUND",
"message": "Agent not found"
},
"request_id": "req_a1b2c3d4"
}Branch on type for broad handling and code for specific cases. message is written
for a human reading a log — do not parse it, and do not show it to your end users
verbatim.
Error types
| Type | Status | What it means |
|---|---|---|
invalid_request_error | 400 | The request was malformed or failed validation |
authentication_error | 401 | The API key is missing, malformed, revoked, or expired |
permission_error | 403 | The key is valid but lacks the required scope |
not_found_error | 404 | No such resource in your organization |
conflict_error | 409 | The resource is not in a state that allows this action |
rate_limit_error | 429 | Too many requests for this key — see rate limits |
api_error | 5xx | Something went wrong on our side |
A 404 may mean "not yours"
Requesting a resource that exists but belongs to another organization returns 404, not
403. This is deliberate: a 403 would confirm the id exists, letting anyone probe for
valid ids across tenants. Treat 404 as "no such resource for this key".
Handling failures well
Retry 429 and 5xx, never 4xx. A 400 will fail identically no matter how many
times you send it. Back off exponentially and cap your attempts.
Treat 409 as a state problem, not a transient one. CALL_NOT_ACTIVE means the call
already ended; retrying will not change that. Re-read the resource and decide again.
async function callApi(path, init, attempt = 0) {
const response = await fetch(`https://api.rabbitt.ai/api/v1${path}`, {
...init,
headers: {
Authorization: `Bearer ${process.env.RABBITT_API_KEY}`,
"Content-Type": "application/json",
...init?.headers,
},
});
if (response.ok) return response.json();
const { error, request_id } = await response.json();
// Only transient failures are worth retrying.
const transient = response.status === 429 || response.status >= 500;
if (transient && attempt < 4) {
await new Promise((r) => setTimeout(r, 2 ** attempt * 500));
return callApi(path, init, attempt + 1);
}
throw new Error(`${error.code}: ${error.message} (request_id: ${request_id})`);
}Common codes
| Code | Status | Usually means |
|---|---|---|
INVALID_API_KEY | 401 | Check the Authorization header and that the key is not revoked |
PERMISSION_DENIED | 403 | The key needs an additional scope |
AGENT_NOT_FOUND | 404 | Wrong agent id, or it belongs to another organization |
ORGANIZATION_AGENT_LIMIT | 409 | Your plan's agent quota is full — delete one or upgrade |
INSUFFICIENT_CREDITS | 402 | Top up before placing more calls |
CALL_NOT_ACTIVE | 409 | The call already ended |
INVALID_AGENT_TYPE | 400 | Interviews need a voice agent; calls need a phone agent |
NO_ALLOWED_ORIGINS | 403 | Add embed origins to the API key before minting embed tokens |
BYOK_NOT_READY | 400 | Creating a BYOK key before you have a verified provider key for the voice LLM, speech-to-text, and text-to-speech engines — add them in billing settings |
BYOK_PROVIDER_KEY_REQUIRED | 400 | An agent was set to a provider your organization has no BYOK key for; add the key or choose a configured provider |
MISSING_BYOK_CREDENTIAL | 400 | An interview or call tried to run on a BYOK key whose provider credential is missing or was removed after the agent was configured |