Errors, retries, and idempotency
Every non-edge API error uses one closed envelope and every application-level response carries Chia-Request-Id:
{
"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.
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; oroperationId: persist the UUIDv4 before the first call and configure the runtime client with a durableidempotencyStorethat 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_typerequest; - 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.
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 on429.
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 for symptom-level diagnosis and the safe escalation packet.