Keyring
ReferenceControl-plane API

Idempotency

The claim, complete, release and heartbeat calls behind `Idempotency-Key`.

Generated from 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 for KEYRING_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 for KEYRING_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 for KEYRING_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 for KEYRING_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']);

On this page