Authentication and errors
API keys
Send your key as Authorization: Bearer <key>. Keys start with sk_ followed by 40 letters and digits. One key grants every
endpoint; keys have no scopes.
Create and revoke keys on the API keys page of the dashboard. A revoked key stops working within a minute. A key spends your account's credits, so keep it on servers and out of code you ship to browsers or apps.
Error format
Errors carry a stable code to branch on, a message for people reading logs, and details where the code has any:
{
"error": {
"code": "CONCURRENT_LIMIT_EXCEEDED",
"message": "Concurrent limit exceeded",
"details": { "limit": 1 },
"timestamp": "2026-09-15T12:00:00.000Z"
}
}
Validation failures use a different shape, with one entry per invalid field:
{
"success": false,
"error": "Request validation failed",
"details": [{ "field": "prompt", "message": "Prompt cannot be empty" }]
}
Items of a batch fail inside a 200 response instead, with codes VALIDATION_ERROR, RESOURCE_ALREADY_EXISTS
or INSUFFICIENT_CREDITS; see batches.
Error codes
| Status | Code | Meaning |
|---|---|---|
| 400 / 422 | — |
The body failed validation: 400 on /v1/monitor/*, 422 on /v1/async/*. details lists each field. |
| 400 | BAD_REQUEST |
The request could not be read. |
| 401 | MISSING_API_KEY |
No bearer token in the Authorization header. |
| 401 | INVALID_API_KEY_FORMAT |
The token is not shaped like an API key. |
| 401 | INVALID_OR_EXPIRED_API_KEY |
The key is unknown or revoked. |
| 403 | INSUFFICIENT_CREDITS |
Your balance does not cover the request's maximum cost. |
| 404 | RESOURCE_NOT_FOUND |
Unknown route or task id, or an engine your deployment does not serve (as a task, that engine fails validation). |
| 405 | METHOD_NOT_ALLOWED |
The path exists, but not with this method. |
| 409 | RESOURCE_CONFLICT |
A task with this idempotencyKey already exists. |
| 413 | PAYLOAD_TOO_LARGE |
The body is larger than the API accepts. |
| 415 | UNSUPPORTED_MEDIA_TYPE |
The body was not sent as application/json. |
| 429 | RATE_LIMIT_EXCEEDED |
Too many requests in the current one-second window. |
| 429 | CONCURRENT_LIMIT_EXCEEDED |
A synchronous call found every concurrency slot busy; details.limit is your limit. |
| 429 | QUEUE_LIMIT_EXCEEDED |
The tasks would overflow your queue; the whole submission is rejected. |
| 429 | SERVICE_OVERLOADED |
The API shed the request under load. Your limits are untouched, so no X-RateLimit-* headers are sent; Retry-After says how long to wait. |
| 500 | INTERNAL_SERVER_ERROR, or the error "Maximum retries exceeded" |
The request failed after automatic retries or ran out of time. No credits are charged. |
| 502 | EXTERNAL_SERVICE_ERROR |
The engine failed in a way retrying would not fix. No credits are charged. |
Retrying safely
- 429 is always safe to retry: the request was not admitted, so nothing ran or was charged. A
SERVICE_OVERLOADED429 carriesRetry-Afterin whole seconds — wait at least that long. The other 429s carry noRetry-After; back off with jitter. A rate-limit 429 clears within a second, a concurrency 429 when a running job finishes. - 5xx, timeouts and connection errors are safe to retry for
GETandDELETE, and for task creation when every task carries anidempotencyKey. A repeat of a task that was created answers409 RESOURCE_CONFLICT(in a batch,RESOURCE_ALREADY_EXISTS): the task exists. - Synchronous monitor calls are charged on success and may still be running when a connection drops. Retry them only when the request never reached the server (connection refused, host not resolved). A
500or502from them is final: the server has already retried. - Other 4xx will fail the same way again; fix the request.
The SDKs apply exactly this policy, with 2 retries by default (maxRetries / max_retries), capped exponential backoff with full jitter,
and per-attempt timeouts of 30 seconds, or 360 seconds for synchronous calls (timeoutMs and syncTimeoutMs / timeout and
sync_timeout). Errors they give up on are raised as ApiError:
import { ApiError, Client } from "@answerline/sdk";
try {
await client.chatgpt({ prompt: "iPhone 17 or Galaxy S26?", country: "US" });
} catch (err) {
if (!(err instanceof ApiError)) throw err;
console.error(err.status, err.code, err.meta.requestId, err.body);
}
from answerline import ApiError
try:
client.chatgpt("iPhone 17 or Galaxy S26?", "US")
except ApiError as err:
print(err.status, err.code, err.meta.request_id, err.body)
Request ids
Every response carries an X-Request-Id header. Send your own (1 to 128 letters, digits, -, _, . or
:) to tie a request to your logs; otherwise one is generated. Quote it when you contact support. The SDKs
send yours through the headers option of request, expose it as meta.requestId / meta.request_id and include it in ApiError messages.
curl -i "https://api.answerline.dev/v1/credits" \ -H "Authorization: Bearer $API_KEY" \ -H "X-Request-Id: nightly-run-42"
const { data, meta } = await client.request("GET", "/v1/credits", undefined, {
headers: { "X-Request-Id": "nightly-run-42" },
});
console.log(meta.requestId);
data, meta = client.request("GET", "/v1/credits", headers={"X-Request-Id": "nightly-run-42"})
print(meta.request_id)