AI

AI Errors and Retries

Every GET /v1/ai error status, what it costs in AI credits, how to retry it, the latency budget, and what to include in a support request.


/v1/ai uses the standard CurrencyCore error envelope, plus a set of 200 responses that are not direct answers. This page separates the two, gives the credit cost of each case, and sets a retry policy that does not burn credits.

Error envelope

{
  "error": {
    "code": "unsupported_query",
    "message": "This endpoint only answers currency conversion, exchange-rate, and PPP questions."
  }
}

code is stable and machine-readable. message is for humans. details is optional and is not used by the AI route today.

Error matrix

StatusCodeWhen it happensAI creditsRetry?
400invalid_inputq is missing or blank.0No. Send a question.
401unauthorizedKey missing, invalid, or revoked. Revocation clears within 5 minutes because of the auth cache.0No. Fix the key.
402billing_past_dueThe organization’s billing is past due.0No. Clear payment.
403forbiddenAI is disabled for the organization, or disabled platform-wide.0No. Enable it in the dashboard, or contact support if the platform switch is off.
413invalid_inputq or total model input exceeds the token budget. Rejected before the model.0No. Shorten the question.
422unsupported_queryThe question is off-topic or an attempt to change the assistant’s behavior. The model was consulted.1 (native)No. Reformulate as a currency, FX, or PPP question.
429rate_limitedPer-minute rate limit, monthly request quota (BYOK tool-call units), or AI credit pool is exhausted.0Yes for the per-minute limit. For credits, wait or buy more.
500internal_errorAn unexpected server error.0Yes, with backoff.
503internal_errorNo provider configured, every provider unreachable, or BYOK required with fallback off but unusable.0Yes for a transient outage. Fix BYOK if it persists.

A 429 from the per-minute rate limit includes Retry-After in seconds. A 429 from an exhausted AI credit pool may not include it, because waiting is not what fixes it. Read the message.

A native 422 is metered because the model did real work before declining. A 413 is not metered because it is rejected before the model runs. 503 is not metered. See AI credits for the pool rules.

A 200 is not always an answer

These cases return 200. There is no outcome or kind field in the body today, so inspect data and answer.

BodyMeaningWhat to do
data non-emptyA normal answer with structured results.Render answer; use data for numbers.
data: [], answer asks for a missing detailClarification. For example, a salary question with no amount, or a multi-country currency with no default.Ask the user for the detail, then send a new request.
data: [], answer explains missing dataIn-scope question, but no rate, PPP factor, currency, or date in coverage.Show the explanation. Do not retry the same question.
data: [], answer asks you to upgradeA Free or Starter organization asked for Growth+ capability.Upgrade, or ask a single-date question.
data: [], toolCalls: 0, answer asks you to narrow the requestThe analysis hit the orchestration deadline. Not a 504.Narrow the window or the currency set.
data: [], answer declines a PPP rankingCross-country purchasing-power ranking is not answerable from PPP factors alone.Ask for a pairwise PPP comparison instead.
data: [], answer gives a one-line topic refusalA related financial topic, such as gold or oil, that the fiat-only data does not cover.Show the refusal. Use a source that covers that asset.
data: [], answer is plain prose with no toolThe answer did not need a data lookup.Treat it as informational, not as a source of numbers.

A 200 with data: [] still consumes one AI credit on the native path, because the model was consulted. A too-large analysis with toolCalls: 0 is the same: one native credit.

Retry policy

GET /v1/ai is safe to retry at the HTTP level, but a successful retry is a new request. On the native path it consumes another AI credit. With BYOK it bills tool calls again. Do not retry a 200 just because you did not like the answer.

type RetryDecision = "retry" | "do-not-retry";

export function retryDecision(status: number): RetryDecision {
  if (status === 429 || status === 500 || status === 503) return "retry";
  return "do-not-retry";
}

export async function askWithRetry(
  q: string,
  key: string,
  maxAttempts = 3,
): Promise<Response> {
  let last: Response | undefined;

  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    const url = new URL("https://api.currency-core.com/v1/ai");
    url.searchParams.set("q", q);

    const res = await fetch(url, {
      headers: { Authorization: `Bearer ${key}` },
      signal: AbortSignal.timeout(30_000),
    });

    if (res.ok) return res;
    last = res;

    if (retryDecision(res.status) === "do-not-retry") return res;

    const retryAfter = Number(res.headers.get("Retry-After"));
    const waitMs = Number.isFinite(retryAfter) && retryAfter > 0
      ? retryAfter * 1_000
      : Math.min(8_000, 500 * 2 ** (attempt - 1)) + Math.random() * 250;

    if (attempt === maxAttempts) return res;
    await new Promise((resolve) => setTimeout(resolve, waitMs));
  }

  return last!;
}

Rules:

  • Do not retry 400, 401, 402, 403, 413, or 422. The request will fail the same way, and a native 422 costs a credit each time.
  • Retry 429 only after Retry-After. If the message says the AI credit balance is exhausted, stop and buy credits, wait for the cycle, or use BYOK with fallback disabled.
  • Retry 500 and 503 with jittered exponential backoff, a few attempts.
  • Do not retry a client-side timeout without a new decision. The server can take up to 15 seconds per attempt, and a lost response may still have been metered.
  • Treat a 200 as final. If you need a different window or shape, send a new prompt, not a retry of the same one.

Latency budget

BudgetDefaultNotes
Orchestration deadline15 seconds per attemptAI_TIMEOUT_MS can change it for a deployment.
Per-provider timeout8 secondsAI_PROVIDER_TIMEOUT_MS can change it. Must stay below the orchestration deadline.
AttemptsUp to 3Only for transient provider unavailability (network, 429, 5xx). Deterministic errors and deadline timeouts are not retried.
Client timeout30 seconds suggestedCovers the 15-second deadline plus retries and network overhead.

If the orchestration deadline is reached and the model produced usable text, the request can still return 200. If it produced no usable answer, the route returns a structured 200 asking you to narrow the request, not a 504. A genuine infrastructure failure returns 503.

There is no published p50 or p95 latency commitment. Simple conversions are normally faster than multi-currency analytics. A request that fans out to many currencies or a long window is the most likely to hit the deadline.

Request IDs

There is no request ID header on /v1/ai today. Do not look for x-request-id or a requestId body field.

The route does return diagnostic headers you can capture:

  • x-currencycore-ai-byok
  • x-currencycore-ai-provider
  • x-currencycore-ai-attempts
  • x-currencycore-ai-tools
  • x-currencycore-ai-fast
  • x-currencycore-ai-ms
  • Server-Timing

These identify routing and timing, not a specific log record. For support, pair them with the UTC time.

Support checklist

Include all of the following in a support request. Do not include the full API key.

  1. The UTC timestamp of the request, with the timezone.
  2. The HTTP status, error code, and full error message.
  3. The exact q value, or a minimal version that still reproduces the issue.
  4. The organization id, and the key prefix only (for example cc_live_ab12).
  5. Whether BYOK was active, and which provider id.
  6. The response headers listed above, especially x-currencycore-ai-byok, x-currencycore-ai-provider, and x-currencycore-ai-attempts.
  7. The full response body if it was a 200 non-answer, with any sensitive text removed.
  8. Whether the issue reproduces with the same q, and roughly how often.
  9. For BYOK: the result of Test connection, the model id, and whether fallback is on. Do not send the provider key.

A support request that says only “the AI gave a wrong answer” is hard to act on. A request that includes the data object, the asOf date, and the REST response you compared against is actionable.