# Errors, retries and version conflicts

> Read structured API errors, recover uncertain requests with stable idempotency keys, reconcile version conflicts, and avoid duplicating side effects or spend.

Source: https://kaitoi.io/docs/core/errors-and-retries/
Section: Kaitoi Core / REST API
Written for: Developers implementing robust API error and retry handling.
Last reviewed: 2026-10-24

---

Treat a failed response as evidence about one request, not proof that no
work happened. Keep the operation identity, request body, version, logical
retry key, and any returned run or task ID.

## Choose recovery from the actual error


The illustration compares a stale project version, a mutation in progress, an unresolved mutation and unavailable save state. Reread and compare a version conflict; inspect the original in-progress request. Preserve local work and request identity for unresolved or unavailable outcomes. Where automaticRetryAllowed is false, the error is not permission to resubmit automatically. The rest of this guide also covers validation and other failures.

The local scale change from 2 to 3 remains visible in every recovery branch. A version conflict compares its observed base 7 with server version 8; other branches retain an original request receipt, unresolved evidence or a held write.

*Figure: Distinguish correcting a request, reconciling state and waiting.*

## Read the structured error

Public errors use `{error:{code,message,details,requestId?}}`. Branch on the
code and operation state; do not scrape human-readable message text. Public
request validation normally uses HTTP 400 `VALIDATION_ERROR`; upload-integrity
completion can use its documented 422 response.

| Status | Investigate                                                           |
| ------ | --------------------------------------------------------------------- |
| 401    | Missing, expired, revoked or invalid credential                       |
| 403    | Scope, capability, origin or resource authority                       |
| 404    | Missing or unavailable/not-owned resource                             |
| 409    | Stale version, conflicting key/body, or request still in progress     |
| 413    | Request or upload size                                                |
| 429    | Rate limit; honor Retry-After when present                            |
| 503    | Unavailable prerequisite such as shared limiting or execution service |

Storage and transfer operations can have additional documented codes. An
authorization refusal is not fixed by submitting the same paid work under
a new key.

## Retry the same logical operation

On routes supporting idempotency, use the same authenticated API-key identity,
route, key, and unchanged request body for the same logical retry. Keep a key
generated once, rather than generating one inside every repeated curl call.
Different content is a different logical operation and conflicts with reuse
of the old key.

Completed-key retention is bounded, currently seven days. Protected unresolved
project-effect claims do not expire into permission to repeat the write.
Waiting for expiry, canceling, or minting a new key is not recovery authority.
Other idempotency behavior is route-specific; it is not indefinite duplicate
protection for every POST, failed job, or replacement credential. After
uncertainty, read the original resource/run/task before deciding what to do.

## Recover a guarded project save

Read the exact code as well as the HTTP status:

| Code                                                            | Next action                                                                       |
| --------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| `PROJECT_VERSION_CONFLICT` (409)                                | Read the current document, compare changes and reconcile.                         |
| `PROJECT_MUTATION_IN_PROGRESS` (409)                            | Preserve local work and inspect the original operation.                           |
| `PROJECT_MUTATION_UNRESOLVED` (409)                             | Preserve work; inspect the original outcome or deliberately save a separate copy. |
| `PROJECT_SAVE_UNAVAILABLE` or `PROJECT_STATE_UNAVAILABLE` (503) | Preserve local work and inspect the guarded state; do not automatically resubmit. |

The latter three cases carry `details.automaticRetryAllowed:false`. They are
different from an ordinary stale version. Do not repeatedly reload, discard
local edits, or submit the same uncertain write under new keys to get past them.

## Reconcile a version conflict

Fetch the current document, inspect intervening changes, and rebuild the
intended update against that state. Do not attach a current expectedVersion
to an old full replacement body. A response failure can occur after persistence;
inspect the resource before assuming all earlier writes rolled back.

## Choose a recovery path

**Uncertain submission:** inspect the original attempt and retry supported
requests with the unchanged body and original logical key. **Version conflict:**
read and reconcile the current document before another write. **Authorization
refusal:** repair the credential, scope, capability or origin; a new paid
request key does not grant access. Errors do not guarantee side-effect rollback.

## Keep side effects and accounting visible

Cancellation is a separate request and does not promise a refund or reversal
of external effects. Back off according to response guidance when retrying
temporary failures. Keep real credentials and private payloads out of logs;
retain sanitized request IDs and error details for support.

See [Runs](/docs/core/runs-and-files/), [Files](/docs/core/files-and-outputs/),
and [Projects](/docs/core/projects-and-graphs/) for operation-specific recovery.
