Error Handling
Error response format, common error codes, and a retry-handling example.
The golden rule: branch on the HTTP status code, not on error text. The text is there to help humans and can change.
What an error looks like
{
"error": {
"message": "Rate limit exceeded. Please upgrade your plan.",
"type": "rate_limit_error",
"code": "rate_limit_exceeded"
}
}Some errors include extra fields: upgradeRequired, requiredPlan and currentPlan when a feature needs a higher plan, or limitExceeded, currentCount and maxChats when you hit a quota. Treat them as helpful detail, not a stable contract.
Status codes
| Status | Code / cause | What to do |
|---|---|---|
| 401 | Missing, malformed, disabled or expired API key ("API key required", "Invalid API key format", "API key is disabled", "API key has expired") | Fix or replace the key. Expired keys are switched off on that request. |
| 403 | Plan gate: upgradeRequired | The feature needs a higher plan (e.g. GPU Tunnels need Pro). The body has requiredPlan and currentPlan. Note: GPU Tunnel / hosted-model management is dashboard-only, so an API-key caller gets 401 there, not 403. |
| 429 | rate_limit_exceeded | Monthly quota or short burst limit. A quota 429 has no Retry-After and resets next month. See Plan Limits. |
| 402 | INSUFFICIENT_CREDITS | The account is out of credits. |
| 400 | INVALID_REQUEST | The request body is malformed or missing required fields. |
| 400 | CONTEXT_LENGTH_EXCEEDED | The message is longer than the model's context window. |
| 404 | CONVERSATION_NOT_FOUND | The RAG conversation ID doesn't exist. |
| 503 | MODEL_NOT_AVAILABLE | The model is temporarily unavailable. Retry, or try another model. |
| 500 | INTERNAL_ERROR | Something went wrong on our side. Retry. |
Handle errors in code
Retry only 429 and 5xx, a limited number of times, with jitter, and respect Retry-After when it's there. Surface everything else.
const MAX_RETRIES = 4;
async function handleTarqaRequest(apiKey, requestData, attempt = 0) {
const response = await fetch('https://api.tarqaai.com/api/v1/chat/completions', {
method: 'POST',
headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
body: JSON.stringify(requestData),
});
if (response.ok) return response.json();
const body = await response.json().catch(() => ({}));
const detail = body.error?.message || body.error || `HTTP ${response.status}`;
// Retryable: rate limits and temporary server errors
if ((response.status === 429 || response.status >= 500) && attempt < MAX_RETRIES) {
const retryAfter = Number(response.headers.get('Retry-After'));
const backoff = Number.isFinite(retryAfter) && retryAfter > 0
? retryAfter * 1000
: Math.min(2 ** attempt * 1000, 30000) + Math.random() * 1000; // jitter
await new Promise(r => setTimeout(r, backoff));
return handleTarqaRequest(apiKey, requestData, attempt + 1);
}
// Not retryable: tell the caller
if (response.status === 401) throw new Error('Auth failed — check your API key.');
if (response.status === 403) throw new Error(`Plan gate: ${detail}. Upgrade required.`);
throw new Error(`API error ${response.status}: ${detail}`);
}Safe retries (Idempotency-Key)
Add an Idempotency-Key header (any unique string, like a UUID) to POST /v1/chat/completions or POST /v1/ask. If you retry with the same key and the same body, you get the original response back instead of running the request again. Replays carry Idempotency-Replayed: true.
| Status | Meaning |
|---|---|
| 200 | Replayed: identical to the first response, plus Idempotency-Replayed: true. |
| 409 | The first request with this key is still running. Wait a moment and retry. |
| 422 | This key was already used with a different body. Use a new key. |
Keys are remembered for 24 hours; after that a repeat runs again. Streaming requests aren't covered: the header is ignored, so a retried stream re-runs tool calls and counts usage again, and tokens from a stream that dropped midway are still billed. Treat a dropped stream as possibly billed.
Errors inside a stream
After a stream starts (HTTP 200), an error arrives as a message in the stream, and its shape depends on the endpoint:
data: {"error":{"message":"upstream provider error","type":"api_error"}}Check every message for these before treating it as text, and handle the connection simply dropping (no [DONE], no error message). Streams can't be resumed (there's no Last-Event-ID), so retry the whole request.
Checklist
- Log the request ID: support will ask for it.
- Check the status code first.
- Validate your request before sending it.
- Watch error rates in Monitor → Analytics.
- Show users a friendly message, not the raw error.