Skip to content
Reliability3 min

Handle errors and retry safely

Read the error format, retry only what is safe to retry, respect rate limits, and use idempotency keys so a retry never creates a second charge.

You need

  • A Tovano API key in TOVANO_API_KEY

Networks fail, providers have bad minutes and traffic comes in bursts. This guide shows which errors to retry, how to wait, and how to make sure a retried request never runs, or charges, twice.

1. The error format

Every error has the same shape:

{
  "error": {
    "type": "invalid_request_error",
    "code": "invalid_request",
    "message": "input.n must be between 1 and 4.",
    "param": "input.n"
  }
}

Every response also has an X-Request-Id header. Log it: it is the fastest way for support to find a request.

2. What to retry

StatusCodeRetry?What to do
401invalid_api_keyNoCheck the key. It may have been revoked
402insufficient_creditsNoAdd credits. See spend limits and alerts
402spend_limit_reachedNoRaise the limit or wait for next month
404not_foundNoCheck the model or task ID
409conflictNoAn idempotency key was reused with a different body
422invalid_requestNoFix the field named in param
429rate_limitedYesWait for Retry-After seconds
500internal_errorYesBack off and retry
502provider_errorYesBack off and retry. The task was not charged

A failed task is never charged, so retrying a provider_error costs nothing extra.

3. Respect rate limits

Limits apply per API key. Responses carry X-RateLimit-Limit and X-RateLimit-Remaining; a 429 carries Retry-After in seconds. Wait at least that long.

Tasks over your concurrency limit are not refused: they wait as queued and start as others finish. For sustained high volume, ask sales@tovano.ai to raise your limits before a launch, not during it.

4. Use idempotency keys

A timeout does not tell you whether the request arrived. If you retry a task creation blindly, you may run it twice and pay twice. An Idempotency-Key header prevents that:

curl https://api.tovano.ai/v1/tasks \
  -H "Authorization: Bearer $TOVANO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1182-hero-image" \
  -d '{ "model": "image/demo-1", "input": { "prompt": "Hero image for order 1182" } }'
  • The same key with the same body returns the first response, marked with the header Idempotent-Replayed: true. No second task is created.
  • The same key with a different body is refused with 409 conflict.
  • Keys are kept for 24 hours, up to 255 characters.

Build the key from your own data, such as an order or job ID and a step name, so that a retry after a crash produces the same key.

5. Back off with jitter

const RETRYABLE = new Set([429, 500, 502, 503]);

async function withRetry<T>(fn: () => Promise<Response>, max = 4): Promise<T> {
  for (let attempt = 0; ; attempt++) {
    const res = await fn();
    if (res.ok) return res.json() as Promise<T>;
    if (!RETRYABLE.has(res.status) || attempt === max) {
      const body = await res.json().catch(() => ({}));
      throw Object.assign(new Error(body.error?.message ?? res.statusText), {
        status: res.status,
        code: body.error?.code,
        requestId: res.headers.get("x-request-id"),
      });
    }
    const after = Number(res.headers.get("retry-after"));
    const wait = after > 0 ? after * 1000 : Math.min(30_000, 500 * 2 ** attempt) * (0.5 + Math.random());
    await new Promise((r) => setTimeout(r, wait));
  }
}

Send the same Idempotency-Key on every attempt. The SDK does both for you: it retries 429, 5xx and network errors with backoff, and adds an idempotency key to task creation automatically.

6. Failed tasks

A task that was accepted can still end as failed. The task's error has a code and a message, and charged_micro is 0. Treat it like a 502: retry with a new idempotency key if the cause was temporary, or fix the input if it was not.

Stuck? Write to support@tovano.ai with the task ID and the X-Request-Id of the response.