Idempotency

Make POSTs safe to retry with the Idempotency-Key header.

Network failures during a POST leave you in the worst-of-both-worlds zone: the call may have succeeded server-side, may have been dropped, or may still be in flight. The Public API solves this with idempotency keys: a client-supplied identifier that lets you safely retry mutating calls without risk of duplicate work.

Sending the header

Pass a unique key in Idempotency-Key on every POST. The header must be between 1 and 255 printable ASCII characters with no whitespace ([\x21-\x7e]{1,255}); a UUID v4 (uuidgen / crypto.randomUUID()) or a hash of the business event are both fine. A bad value is rejected with validation_error (400).

curl -X POST https://api.rankprompt.com/v1/brands \
-H "Authorization: Bearer rp_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"name": "Acme Co"}'
import { randomUUID } from 'node:crypto';

await fetch('https://api.rankprompt.com/v1/brands', {
method: 'POST',
headers: {
  Authorization: `Bearer ${process.env.RANKPROMPT_API_KEY}`,
  'Content-Type': 'application/json',
  'Idempotency-Key': randomUUID(),
},
body: JSON.stringify({ name: 'Acme Co' }),
});
import os, uuid, httpx

httpx.post(
  "https://api.rankprompt.com/v1/brands",
  headers={
      "Authorization": f"Bearer {os.environ['RANKPROMPT_API_KEY']}",
      "Idempotency-Key": str(uuid.uuid4()),
  },
  json={"name": "Acme Co"},
  timeout=30,
)

The header is required on every POST: a POST without Idempotency-Key is rejected with idempotency_key_required (400). For PATCH and DELETE the header is optional but honored: if you send one we’ll deduplicate the call, otherwise the request runs normally. GET ignores the header.

What happens on retry

When the server sees a key it has processed before, it short-circuits the business logic and replays the original response with the same status, the same JSON body, the original Content-Type and quota (X-RP-Quota-*) headers, plus a marker:

HTTP/1.1 201 Created
Content-Type: application/json
X-RP-Quota-Used: 18
X-RP-Quota-Remaining: 982
Idempotent-Replayed: true

{ ...same JSON the first call returned... }

A replay does not debit your quota again. Sensitive headers (Set-Cookie, Authorization, Date) are deliberately stripped from the cached response.

So your “did it actually go through?” recovery flow is just: re-send the same request with the same key. The first call wins; every retry returns the same response shape until the key expires.

Server failures (>= 500) don’t become permanent: the in-flight row is discarded so your next retry with the same key actually re-runs the handler instead of replaying the failure.

Errors you might hit

  • idempotency_key_required (400): POST without the header.
  • validation_error (400): the header didn’t match [\x21-\x7e]{1,255} (no whitespace, 1-255 printable ASCII chars).
  • idempotency_key_mismatch (409): same key reused with a different request body or against a different route. The fingerprint is sha256(method + route_template + body). Pick a fresh key or send the original payload byte-for-byte to the same endpoint.
  • idempotency_key_in_flight (409): you’re racing yourself. A previous request with this key is still being processed; wait a moment and retry with the same key.

TTL & best practices

  • Keys are remembered for 24 hours. After that the slot is freed and the same key is treated as a brand-new request.
  • Generate keys at the business-event level, not at the HTTP level. “create-brand-acme-2026-04-19” is better than a fresh UUID per HTTP retry, because it deduplicates app-level retries too (e.g. a backoff job that fires a second time when the first one’s success is in doubt).
  • For background workers, persist the generated key alongside the job record before the first attempt. That way every retry uses the same key even if the worker crashes and restarts.