Idempotency
The claim, complete, release and heartbeat calls behind `Idempotency-Key`.
packages/api/src/idempotency/idempotency.controller.ts by packages/docs/tools/generate-reference.mjs. Do not edit by hand: src/reference.spec.ts regenerates it and fails on a difference.Report section 4.3's protocol, as four calls.
claim is the one that matters and it is one atomic Redis script: exactly
one of N simultaneous duplicates is told to execute, and the other N-1 are
told the request is already in progress. Returning that rather than blocking
is Stripe's behaviour and the right one -- blocking would hold a connection
in the customer's app for the duration of someone else's slow handler, which
is the resource exhaustion the feature is supposed to prevent.
Like the rate-limit endpoint: no audit row, no advisory lock, and a 200 for every verdict. The 409, the 422 and the replayed status belong to the customer's caller and are issued by the SDK in the customer's process.
POST /v1/idempotency/claim
Answers 200 on success.
Authentication.
- A
krsk_secret key only, resolved once and reused forKEYRING_HOT_AUTH_CACHE_MS(5 s by default).
Body. Validated by this schema, from the control plane's own source:
const IdempotencyClaim = z.object({
scope_hash: Sha256Hex,
/**
* `SHA-256(canonical(method ‖ path ‖ sorted query ‖ body))`. Headers are
* deliberately not in it: a retry legitimately carries a different
* `User-Agent`, `Date` or trace header.
*/
fingerprint: Sha256Hex,
env: Env,
ttl_ms: z.number().int().min(60_000).max(604_800_000).optional(),
});POST /v1/idempotency/complete
Answers 200 on success.
Authentication.
- A
krsk_secret key only, resolved once and reused forKEYRING_HOT_AUTH_CACHE_MS(5 s by default).
Body. Validated by this schema, from the control plane's own source:
const IdempotencyComplete = z.object({
scope_hash: Sha256Hex,
env: Env,
lock_token: z.string().min(8).max(64),
status: z.number().int().min(100).max(599),
headers: z.record(z.string().max(128), z.string().max(4096)).default({}),
/** base64. Absent means the response was over the cap and is not replayable. */
body: z
.string()
.max(6 * 1024 * 1024)
.nullable()
.default(null),
ttl_ms: z.number().int().min(60_000).max(604_800_000).optional(),
});POST /v1/idempotency/release
Report section 4.4's first case, and its one deviation from Stripe: a 5xx the customer's predicate calls transient releases the record so the retry executes, instead of being stored and replayed forever. Their 503 is usually "my database was failing over", not "this operation was attempted".
Answers 200 on success.
Authentication.
- A
krsk_secret key only, resolved once and reused forKEYRING_HOT_AUTH_CACHE_MS(5 s by default).
Body. Validated by this schema, from the control plane's own source:
const IdempotencyLock = z.object({
scope_hash: Sha256Hex,
env: Env,
lock_token: z.string().min(8).max(64),
});POST /v1/idempotency/heartbeat
Keeps a legitimately slow handler from being double-executed at 30 s.
Answers 200 on success.
Authentication.
- A
krsk_secret key only, resolved once and reused forKEYRING_HOT_AUTH_CACHE_MS(5 s by default).
Body. Validated by this schema, from the control plane's own source:
const IdempotencyLock = z.object({
scope_hash: Sha256Hex,
env: Env,
lock_token: z.string().min(8).max(64),
});Shared validators
Defined once in packages/api/src/validation.ts and used by the schemas above.
const Env = z.enum(['live', 'test']);