# Errors, retries, and idempotency Every non-edge API error uses one closed envelope and every application-level response carries `Chia-Request-Id`: ```json { "error": { "type": "rate_limit_error", "code": "rate_limit_exceeded", "message": "Too many requests.", "param": null, "request_id": "req_01K0ABCDEFGH", "retryable": true } } ``` With the SDK, catch `ChiaPartnerError`. Record only `status`, `code`, `retryable`, `param`, and `requestId`. Its message is redacted, but treat it as operational text—not user copy. ```ts try { await runtime.listCatalog({ ifNoneMatch: currentEtag }); } catch (error) { if (error instanceof ChiaPartnerError) { safeMetric("chia_partner_api_error", { status: error.status, code: error.code, retryable: error.retryable, requestId: error.requestId, }); } throw error; } ``` Never log request headers, option objects, response bodies, webhook bodies, credentials, authenticated asset URLs, or handoff URLs/fragments. ## Retry decision table | Status/code | Retry? | Action | |---|---|---| | `400 cursor_expired` | restart traversal | Discard `starting_after`, fetch the first settlement page, insert immutable release IDs idempotently, and reapply `supersedes_id` links | | other `400`, `415` | no | Fix request/content type; contract fields are closed | | `401 invalid_api_key` | no | Check origin/key pairing and revocation/configuration; rotate if exposure is possible | | `403 insufficient_scope` | no | Move the operation to the right isolated role; never substitute a key | | `404 catalog_asset_unavailable` | no | Abort/purge candidate bytes and refetch catalog | | `409 idempotency_key_reused` | no | Same key was used with a different test body; create a new logical operation | | `409 webhook_endpoint_unavailable` | no | Fix registered endpoint readiness before a new test | | `429 rate_limit_exceeded` | yes | Obey `Retry-After`, add jitter, reduce polling | | `500 internal_error`, any `503` with `retryable: true` | yes | Bounded exponential backoff | | opaque edge `502`, `504` | yes | Treat as a transient transport failure; use the same bounded budget | | `network_error`, `request_timeout` | yes | Retry within the same operation budget | | `asset_integrity_error` | no immediate byte reuse | Abort/purge, alert, then refetch the catalog | | `unsupported_api_version`, `invalid_response` | no | Stop publication and investigate signed release/config drift | The SDK defaults to at most three attempts and a ten-second timeout per attempt. It applies jitter and honors a valid integer `Retry-After` up to `maxRetryDelayMs` (30 seconds by default). If Chia asks for a longer wait, the SDK returns the error with `retryAfterMs`; move the operation to a durable queue instead of holding an application request open. Every operation accepts an `AbortSignal`. Keep the complete retry budget bounded. The SDK rejects a sandbox/live key-prefix mismatch locally before sending a request. The API deliberately does not reveal whether an otherwise invalid key belongs to another environment: a direct integration receives `401 invalid_api_key`, not an environment-enumerating error. Catalog refreshes are operational, not interactive. During a retryable failure, continue serving only the last valid unexpired snapshot. A production finance connectivity retry can wait for the next isolated finance schedule. Mobile handoff should not wait on either API. ## Idempotency `GET` catalog, asset, and settlement operations are read-only. Retry them normally; do not send `Idempotency-Key`. `POST /v2/handoff-sessions` requires a caller-persisted random UUIDv4. Reuse it only for the byte-identical closed body after an uncertain transport outcome. A new affirmative action is a new logical operation and key. Never derive the key from, or add to the body, any user/account/device/contact/patient/order value. A definitive `201` returns a 15-minute URL; do not post again, persist the URL, or attempt a status read. `POST /v2/webhook_tests` also requires a cryptographically random UUIDv4 `Idempotency-Key`. The SDK requires exactly one of these two caller-retained inputs: - `idempotencyKey`: generate and persist the UUIDv4 in your operation record before the first call; or - `operationId`: persist the UUIDv4 before the first call and configure the runtime client with a durable `idempotencyStore` that maps that operation and event type to one stable key. Passing both inputs, neither input, or `operationId` without a configured durable store fails locally before any request. The SDK never creates an ephemeral fallback key that disappears when a request fails. For either mode: - create one key for one logical `event_type` request; - retain the caller-owned key or operation ID before the first attempt; - reuse it for transport/time-out retries of the byte-identical body; - never reuse it for another event type or environment; - after a definitive `202`, store the returned acceptance for that operation instead of posting again. ```ts import { randomUUID } from "node:crypto"; const idempotencyKey = randomUUID(); await webhookTestOperations.insert({ environment: "sandbox", eventType: "endpoint.test", idempotencyKey, }); // durable commit before network I/O const accepted = await runtime.createWebhookTest({ eventType: "endpoint.test", idempotencyKey, }); await webhookTestOperations.markAccepted(idempotencyKey); ``` The alternative `operationId` mode is useful when the runtime client already has a transactional or otherwise process-safe `idempotencyStore`. The store reuses one key for the tuple of environment, operation ID, and event type. An in-memory store is test-only and does not satisfy restart-safe delivery. Idempotency of the API request does not replace webhook deduplication. Delivery remains at least once, so the receiver must commit a unique `Chia-Webhook-Id` boundary. ## Rate limits Every application-level API response exposes: - `RateLimit-Limit`: request budget for the credential/partner window; - `RateLimit-Remaining`: budget remaining; - `RateLimit-Reset`: seconds until reset; - `Retry-After`: seconds to wait on `429`. An intermediary-generated `502` or `504` may omit all Chia headers and the JSON envelope. Preserve your own correlation ID and UTC timestamp; do not turn an unvalidated header into a trusted Chia request ID. No fixed numeric allowance is part of this contract. Design around conditional catalog fetches, explicitly requested synthetic webhook tests, scheduled isolated finance connectivity checks, and coalesced refreshes rather than consuming the budget per user. See [Troubleshooting](https://partners.chia.health/docs/troubleshooting/) for symptom-level diagnosis and the safe escalation packet.