Idempotency
Idempotency keys identify one logical command. Exact replays return the original authority or result without duplicating a side effect.
The contract
await client.cart.addItem(
{ item_id: "prod_studio-tee", quantity: 1 },
{ idempotencyKey: "add-tee-2026-05-07-abc123" },
);
await client.cart.applyCoupon("WELCOME10", { idempotencyKey: "coupon-…" });
await client.purchaseIntents.mint({
...mintRequest,
client_intent_key: "purchase-intent-…",
});
await client.uploads.init("hero.png", "image/png", 12_345, { idempotencyKey: "upl-…" });Auto-generation
Omit the option and the SDK mints a UUID for you, transparently. That covers the common case: single-flight writes from a typed client where retries are not yet a concern. Override only when you need cross-process replay safety or a deterministic key.
How replay works on the server
The server stores the response keyed by (business_id, route, idempotency_key) for 24 hours. Subsequent requests with the same key:
| Body | Result |
|---|---|
| Identical to original | Cached response replayed; no side effect |
| Different from original | 409 with code IDEMPOTENCY_KEY_REUSED |
| Original still in flight | 409 with code ALREADY_PROCESSING; caller should poll, not retry |
When to override the default
Double-submit guards
A user double-clicks the "Place order" button. Without an explicit key, two requests each get their own UUID, and the server runs the side effect twice. Bind the key to the user action, not the request.
const checkoutKey = useMemo(() => crypto.randomUUID(), [cartId]);
async function placeOrder() {
const r = await client.purchaseIntents.mint({
...mintRequest,
client_intent_key: checkoutKey,
});
if (!r.ok) return showError(r.error);
renderTermsForAcceptance(r.value.current_terms);
}CI replays
A CI job that adds a fixture cart, then re-runs after a flaky network blip, should converge to the same final state. Use a deterministic key derived from the test run identifier.
const runId = process.env.GITHUB_RUN_ID ?? "local";
await client.cart.addItem(
{ item_id: "prod_x", quantity: 1 },
{ idempotencyKey: `fixture-cart-${runId}` },
);Webhook receivers
On the inbound side, treat each event's id as the idempotency key for the operation you trigger downstream. Same fact, same fingerprint, same result.
Don't reuse keys across resources
Idempotency is scoped by the owning resource and command. A Cart write and a purchase-intent mint can use the same text without colliding, but one purchase-intent key must never represent two different carts or buyer inputs. Preparation keys are exact receipt identities: reuse the same key after an ambiguous response, and rotate it after an explicit material or tender edit.
Methods that accept idempotency keys
| Service | Methods |
|---|---|
client.cart | addItem, applyCoupon |
client.purchaseIntents | mint, hosted prepare, confirm, owner-fenced action submission |
client.auth | requestOtp, verifyOtp |
client.orders | cancel |
client.subscriptions | cancel, skipNext |
client.scheduling | cancelBooking, rescheduleBooking |
client.uploads | init |
client.fx | lockQuote |
Next
Result
Every SDK method returns `Result<T, CimplifyError>` and never throws. You narrow on `.ok`, and TypeScript gives you either `value` or `error` on the other branch.
Errors
SDK methods never throw. They return a `Result` whose error branch is a typed `CimplifyError` with a stable `code`, a human-readable `message`, an optional `retryable` hint, and structured `context`.