Reference

Idempotency

Safely retry POST requests without creating duplicate resources using the Idempotency-Key header.

Last updated 2026-06-11

The Core API supports the Idempotency-Key header on POST requests. When a client retries a request with the same key, the server replays the original response instead of re-executing the operation. This prevents duplicate resource creation caused by network timeouts, AI agent retries, or webhook handler failures.

Quick start

Add an Idempotency-Key header to any POST request. The key must be a unique string between 1 and 256 characters. UUIDs work well.

curl -X POST https://api.authpi.com/v1/accounts/{account_id}/issuers \
  -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
  -H "Content-Type: application/json" \
  -d '{"name": "my-issuer"}'

If the request succeeds and you send the exact same request again with the same key, the server returns the cached response with an additional header:

Idempotent-Replayed: true

The response body and status code are identical to the original.

How it works

Lifecycle of an idempotency key

  1. First request — The server records the key, executes the handler, and caches the response.
  2. Replay — A subsequent request with the same key and body returns the cached response immediately. Authorization scopes are still checked, but the handler is not re-executed and no credits are charged.
  3. Expiry — Keys expire automatically after 24 hours. After expiry, the same key can be reused.

Scope

  • Keys are scoped per account. The same key used by different accounts will not collide.
  • The request fingerprint includes both the URL path and the request body. The same key used on different endpoints (e.g., /webhooks vs /issuers) is treated as a mismatch.

What gets cached

Only successful responses (2xx status codes) are cached. If the original request fails with a 4xx or 5xx error, the key is released so you can retry.

Responses larger than 512 KB are marked as completed but the body is not stored. A replay will return the original status code but with an empty body.

Error responses

422 — Body mismatch

If you reuse a key with a different request body, the server returns 422 Unprocessable Content:

{
  "error": "invalid_request",
  "error_description": "Idempotency-Key has already been used with a different request body"
}

This protects against accidental key reuse. Always generate a new key for each distinct operation.

409 — Concurrent request

If two requests with the same key arrive simultaneously, the second one receives 409 Conflict with a Retry-After header:

{
  "error": "conflict",
  "error_description": "A request with this Idempotency-Key is currently being processed"
}

Wait for the duration specified in Retry-After (typically 5 seconds), then retry.

400 — Invalid key

Keys must be between 1 and 256 characters. Empty keys or keys exceeding 256 characters return 400 Bad Request.

Secret-returning endpoints

Endpoints that return one-time secrets are excluded from idempotency caching. These endpoints document that the secret is “returned once” and cannot be retrieved again. Caching the response would break that guarantee.

The following POST endpoints are excluded:

EndpointSecret field
Create API keysecret_plain
Rotate API key secretsecret_plain
Create personal tokentoken_plain
Create client (confidential)secret
Rotate client secretsecret
Create webhook (bearer/signature auth)bearer_token_plain / signature_secret_plain
Create agent verifier (secret type)secret
Regenerate backup codesplaintext codes
Complete TOTP enrollmentbackup codes

For these endpoints, the Idempotency-Key header is accepted but the response is not cached. Retrying will re-execute the handler, and the resource’s own duplicate-prevention logic will apply.

Non-POST methods

The Idempotency-Key header is silently ignored on GET, PATCH, and DELETE requests. These methods are naturally idempotent by HTTP semantics:

  • GET — Read-only, always safe to retry.
  • PATCH — Applies the same update, producing the same result.
  • DELETE — Deleting an already-deleted resource returns 404.

Best practices

Generate a unique key per operation

Use a UUID v4 or another high-entropy random string. Never reuse keys across different operations.

const key = crypto.randomUUID();

Store the key with your request

If your client might crash mid-request, persist the idempotency key alongside the request parameters before sending. On restart, replay with the same key.

// Before sending
const key = crypto.randomUUID();
await db.insert({ key, action: "create_user", body });

// Send (safe to retry on failure)
const res = await fetch(url, {
  method: "POST",
  headers: { "Idempotency-Key": key, ... },
  body: JSON.stringify(body),
});

// After confirmed success
await db.delete({ key });

Handle the Idempotent-Replayed header

Check for the Idempotent-Replayed: true response header to distinguish replays from original responses in your logging:

const replayed = res.headers.get("Idempotent-Replayed") === "true";
if (replayed) {
  console.log("Response was replayed from cache");
}

Retry strategy

Combine idempotency keys with exponential backoff:

async function safePost(url, body, maxRetries = 3) {
  const key = crypto.randomUUID();

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const res = await fetch(url, {
      method: "POST",
      headers: {
        "Idempotency-Key": key,
        "Content-Type": "application/json",
        "Authorization": `Bearer ${token}`,
      },
      body: JSON.stringify(body),
    });

    if (res.status === 409) {
      // Concurrent processing — wait and retry
      const retryAfter = parseInt(res.headers.get("Retry-After") || "5");
      await new Promise(r => setTimeout(r, retryAfter * 1000));
      continue;
    }

    return res;
  }

  throw new Error("Max retries exceeded");
}