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
| Status | Code | Retry? | What to do |
|---|---|---|---|
| 401 | invalid_api_key | No | Check the key. It may have been revoked |
| 402 | insufficient_credits | No | Add credits. See spend limits and alerts |
| 402 | spend_limit_reached | No | Raise the limit or wait for next month |
| 404 | not_found | No | Check the model or task ID |
| 409 | conflict | No | An idempotency key was reused with a different body |
| 422 | invalid_request | No | Fix the field named in param |
| 429 | rate_limited | Yes | Wait for Retry-After seconds |
| 500 | internal_error | Yes | Back off and retry |
| 502 | provider_error | Yes | Back 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.