Passkey-X
Conventions

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.

HeaderAPIsWhat it meansExample
Idempotency-KeyAll (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-ReplayedAll (writes)true when this is the stored first answer to a repeated Idempotency-Key.Idempotent-Replayed: true
Retry a write safely
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

SituationAnswer
Same key, same request, first one finishedThe first answer again, Idempotent-Replayed: true
Same key, first one still running409 idempotency_in_progress, Retry-After: 1
Same key, different body or path422 idempotency_key_reused
Key with spaces or too short400 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.

HeaderAPIsWhat it meansExample
If-MatchCustomer APIThe 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-MatchAll (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"
ETagAll (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.

Retries, idempotency and headers
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.