Idempotency
Network requests fail. Connections time out. Load balancers shed traffic. When you’re issuing a POST that costs money — like creating a scan or kicking off an export — you need to be able to retry safely, without the risk of running the same job twice.
That’s what the Idempotency-Key header is for. Send a fresh UUID per logical operation; retry the request as many times as you want with the same key, and the server will return the original response instead of doing the work again.
The Regulatory Snapshot API follows the Stripe-style idempotency pattern: a 24-hour replay window, a fingerprint check, and clear 409 errors when something has gone subtly wrong.
When to use it
Use an idempotency key on:
POST /v1/scansPOST /v1/exports
Both SDKs auto-generate a UUID4 per call. If you’re going direct over HTTP, generate one yourself — uuidgen on the command line is plenty.
[!WARNING] Reuse keys at your peril. An idempotency key is a per-operation identifier — generate a new one for each new scan you want to run. Reusing a key with a different request body is treated as a programmer error and rejected with
409 idempotency_key_in_use.
How it works
The server keys cache entries on (api_key_id, endpoint, idempotency_key). The cached request fingerprint is the SHA-256 of your canonical (sorted-keys) JSON body. The cached response is the original 2xx body.
Key format
- 1–255 characters
- Printable ASCII (0x20–0x7E) — no Unicode, no tabs, no newlines
- UUID4 is the conventional choice; any opaque, non-reused string works
Malformed keys return 422 invalid_idempotency_key.
The cases, in detail
Same key, same body — cached 2xx
The server returns the exact same response as the first call, including the same scan_id or export_job_id. It also sets the response header Idempotency-Replayed: true so your client knows the work wasn’t re-run.
HTTP/1.1 202 Accepted
Content-Type: application/json
Idempotency-Replayed: true
{ "scan_id": "8f3d…", "status": "running", ... }This is the case you want. Retry safely.
Same key, different body — 409 idempotency_key_in_use
If you send the same key but change anything in the body (even adding a whitespace-different field), you get:
{
"type": "https://api.regsn.app/problems/conflict",
"title": "Conflict",
"status": 409,
"code": "idempotency_key_in_use",
"detail": "This Idempotency-Key was previously used with a different request body."
}The fix is always the same: generate a new key for the new request. The key already in use will expire after 24 hours.
Same key, first request still running — 409 idempotency_in_progress
If the original request is still mid-flight when you retry, you get:
{
"type": "https://api.regsn.app/problems/conflict",
"title": "Conflict",
"status": 409,
"code": "idempotency_in_progress",
"detail": "A request with this Idempotency-Key is still being processed."
}Wait a few seconds and retry. The server will either return the cached 2xx (if the first request finished) or this 409 again (if it’s still going).
Server error (5xx) or validation error — not cached
If the first attempt returned 5xx or a 4xx validation error, the placeholder cache row is deleted. The next request with the same key starts fresh — no replay, no 409.
This is deliberate: caching a 500 would lock you out for 24 hours from running the same input.
TTL
Records expire 24 hours after creation. After that, the same key can be reused for a different operation — though in practice it’s safer to always mint a new UUID.
What the SDKs do
Both the Python and JavaScript SDKs:
- Read your
idempotency_key/idempotencyKeyparameter if provided. - Otherwise, generate a UUID4 per
POSTcall. - On retry (5xx, 429, network), reuse the same idempotency key — that’s the whole point.
If you want explicit control (e.g., to tie the key to your own internal job ID so it survives process restarts), pass it yourself:
client.scans.create(
jurisdictions=["UK"], areas=["AML"], horizon=12,
idempotency_key="weekly-uk-aml-2026-W19",
)await client.scans.create(
{ jurisdictions: ['UK'], areas: ['AML'], horizon: 12 },
{ idempotencyKey: 'weekly-uk-aml-2026-W19' }
);Tying it to fingerprint determinism
There’s a related concept that sometimes confuses people: each scan request also has an implicit input fingerprint (SHA-256 of the canonical body), surfaced as _meta.inputFingerprint on the resulting snapshot. The engine reuses the prior result when the same fingerprint reappears. See Scans → Fingerprint determinism for the full picture.
- Idempotency-Key is a client-supplied identifier with a 24-hour TTL. It guarantees you can retry one specific HTTP request.
- Input fingerprint is a server-derived identifier with no TTL. It guarantees that “the same scan question always produces the same scan answer” — even across days, weeks, and Idempotency-Key expirations.
You generally don’t need to think about fingerprint determinism — the engine handles it. Just send your idempotency key.