Idempotency and concurrency
Retry writes safely with Idempotency-Key, and never overwrite someone else's change with ETag and If-Match.
Idempotency-Key
Send an Idempotency-Key (a UUID is ideal) on every POST, PUT, PATCH and DELETE, and reuse it when you retry. A repeat with the same key and body returns the first answer again with Idempotent-Replayed: true. The Organization API keeps answers 24 hours; for the Customer and Agent APIs the gateway keeps them 10 minutes. Answers with 5xx or 429 are not kept, so the retry runs again.
| Header | APIs | What it means | Example |
|---|---|---|---|
Idempotency-Key | All (POST, PUT, PATCH, DELETE) | 8–128 letters, digits, dots, dashes, underscores or colons. A repeat with the same key and body returns the first answer with Idempotent-Replayed: true (Organization API: kept 24 hours; Customer and Agent API: 10 minutes at the gateway). The same key with another body is 422 idempotency_key_reused; while the first is still running, 409 idempotency_in_progress. An invalid key is 400 invalid_idempotency_key. | Idempotency-Key: 7b4f0c1e-2d3a-4b5c-8d9e-0f1a2b3c4d5e |
Idempotent-Replayed | All (writes) | true when this is the stored first answer to a repeated Idempotency-Key. | Idempotent-Replayed: true |
KEY=$(uuidgen) # one key per logical operation; reuse it on every retry
curl -s -X POST 'https://passkey-x.com/api/org/v1/remote/invites' \
-H "Authorization: Bearer $PASSKEY_X_API_KEY" -H 'Content-Type: application/json' \
-H "Idempotency-Key: $KEY" -d '{"email":"customer@example.com","hours":24}'
# Timed out? Send exactly the same request with the same key: the first answer comes back
# with Idempotent-Replayed: true, and the invite is created once.Every answer
| Situation | Answer |
|---|---|
| Same key, same request, first one finished | The first answer again, Idempotent-Replayed: true |
| Same key, first one still running | 409 idempotency_in_progress, Retry-After: 1 |
| Same key, different body or path | 422 idempotency_key_reused |
| Key with spaces or too short | 400 invalid_idempotency_key |
Codes are explained in Errors.
ETag and If-Match
Reading or writing a vault item returns an ETag: its current revision. Send it back as If-Match on PATCH and DELETE. If someone changed the item in the meantime you get 412 revision_conflict: fetch the latest revision, merge locally, re-encrypt and try again. On reads, send If-None-Match with an ETag you already have: an unchanged answer is 304 Not Modified with no body.
| Header | APIs | What it means | Example |
|---|---|---|---|
If-Match | Customer API | The ETag of the revision you read, on PATCH and DELETE of vault items. A newer revision answers 412 revision_conflict. | If-Match: "3" |
If-None-Match | All (GET) | The ETag of an answer you already have. When nothing changed the answer is 304 Not Modified with no body (it still counts toward the rate limit). | If-None-Match: W/"q2v9X0f3kL1m8n7b6v5c4x" |
ETag | All (GET) | Vault items: the revision ("3"), to send back as If-Match. Other reads: a weak hash of the body (W/"…"), to send back as If-None-Match. | ETag: W/"q2v9X0f3kL1m8n7b6v5c4x" |
In the SDK
The SDK sends an Idempotency-Key on every write (the same on every retry), retries 409 idempotency_in_progress, sends If-Match from the revision you pass, and with cache: true revalidates GETs with If-None-Match.
import { PasskeyX, responseMeta, type Page } from "@passkey-x/sdk";
type Member = { identity_id: string; email: string; role: string; status: string };
const org = new PasskeyX({
apiKey: process.env.PASSKEY_X_ORG_KEY!, // pxk_…
maxRetries: 4, // 429 / 5xx / network: backoff with jitter, honours Retry-After
cache: true, // GETs revalidate with If-None-Match; a 304 reuses the body
onDeprecation: ({ path, sunset }) => console.warn("deprecated", path, "sunset", sunset),
});
// Pagination: every page, following next_cursor (or Link rel="next")
for await (const row of org.org.members.iterate({ limit: 200 })) {
const member = row as unknown as Member;
if (member.status === "suspended") console.log(member.email);
}
// Idempotency: the same key on every retry, and across processes if you pass your own
const page = await org.request<Page<Member>>("POST", "/org/v1/members", {
body: { email: "new.hire@example.com", role: "member" },
idempotencyKey: `invite:new.hire@example.com:2026-10-21`,
});
const meta = responseMeta(page);
console.log(meta?.requestId, meta?.replayed ? "replayed" : "created", org.rateLimit?.remaining, "left");Need help building on Passkey-X?
Questions about the API, a bug or a missing endpoint? Our team replies by email, usually within one business day.