Error Handling

Understand and handle API errors gracefully

Error response format

The shape depends on the endpoint family:

OpenAI-compatible endpoints (/v1/chat/completions)
{
  "error": {
    "message": "Rate limit exceeded. Please upgrade your plan.",
    "type": "rate_limit_error",
    "code": "rate_limit_exceeded"
  }
}
All other Tarqa endpoints
{
  "success": false,
  "error": "API key has expired"
}

Some responses carry extra fields alongside errorupgradeRequired / requiredPlan / currentPlan on a plan gate, limitExceeded / currentCount / maxChats on a quota stop. Always branch on the HTTP status code first; treat the string fields as human-readable detail, not a stable API.

Common error codes

401
Missing / malformed / disabled / expired API key
All key failures return 401 — "API key required", "Invalid API key format", "API key is disabled", or "API key has expired". Expired keys auto-deactivate on that request.
403
Plan gate (upgradeRequired)
Feature needs a higher plan (e.g. Hosted Models / GPU Tunnels need Pro). Body has upgradeRequired, requiredPlan, currentPlan. Note: Hosted Models / GPU Tunnels are session-only — an API-key caller gets 401 there, not 403.
429
rate_limit_exceeded
Monthly request or token quota reached, or a short-term per-minute burst limit. See Plan Limits. The monthly-quota 429 has no Retry-After — it resets at the month boundary.
402
INSUFFICIENT_CREDITS
Account has run out of credits
503
MODEL_NOT_AVAILABLE
Requested model is temporarily unavailable
400
INVALID_REQUEST
Request body is malformed or missing required fields
400
CONTEXT_LENGTH_EXCEEDED
Message exceeds the model context window
404
CONVERSATION_NOT_FOUND
RAG conversation ID does not exist
500
INTERNAL_ERROR
Internal server error — please retry

Error handling example

Branch on the HTTP status, not on a string. Retry only 429 and 5xx, with a bounded count and jitter, and honour Retry-After when the server sends it.

JS
const MAX_RETRIES = 4;

async function handleTarqaRequest(apiKey, requestData, attempt = 0) {
  const response = await fetch(`${BASE_URL}/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 transient 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 — surface it.
  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

Send an Idempotency-Key header (any unique string, e.g. a UUID) on POST /v1/chat/completions and POST /v1/ask. A retry that carries the same key and the same request body returns the stored original response instead of running again — the replay carries Idempotency-Replayed: true.

200Replayed — identical to the first response, plus header Idempotency-Replayed: true
409The first request with this key is still in flight — retry after a short wait
422This key was already used with a different request body — use a new key

Keys are remembered for 24 hours, then a repeat re-executes. Streaming requests (stream: true) are not covered — the header is ignored, so a retried stream re-runs tool calls and re-counts usage, and tokens from a stream that dropped mid-way are still billed. Treat a dropped stream as possibly-billed.

Errors inside a stream

Once a streaming response has started (HTTP 200), a failure arrives as a frame, not a status code — and the shape differs by endpoint:

/v1/chat/completions (OpenAI-style)
data: {"error":{"message":"upstream provider error","type":"api_error"}}
/v1/ai/* and /v1/context/* (Tarqa-style)
data: {"type":"error","error":"upstream provider error"}

Your stream consumer must check for these before treating a frame as content, and must handle the connection simply dropping (no [DONE], no error frame). There is no resumption — no Last-Event-ID; retry the whole request (see the caveat above).

Debugging tips

Always log the requestId for support · check response status codes first · validate request format before sending · monitor error rates in analytics

Production best practices

Branch on HTTP status, not error strings · bound your retries and add jitter · set up error monitoring · give users friendly error messages