Skip to Content

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/scans
  • POST /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:

  1. Read your idempotency_key / idempotencyKey parameter if provided.
  2. Otherwise, generate a UUID4 per POST call.
  3. 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.

See also

  • Scans — idempotency on POST /v1/scans.
  • Exports — idempotency on POST /v1/exports.
  • Errors — 409 idempotency_key_in_use, 409 idempotency_in_progress, 422 invalid_idempotency_key.
  • Glossary — idempotency key, fingerprint.