Scans
A scan is a request for a fresh look at the regulatory landscape for a specific set of jurisdictions and topics over a horizon (in months). The output of a scan is a snapshot — a structured envelope of items, trends, and an executive narrative. This page covers everything you need to drive a scan from start to finish: the request body, the two execution modes, real-time progress streaming, and the determinism guarantees that make retries safe.
If you’ve never made a request before, start with the Quickstart. The rest of this page assumes you’ve got a working regsn_live_ bearer key in hand.
The shape of a request
{
"jurisdictions": ["UK", "EU"],
"areas": ["AML", "Sanctions"],
"horizon": 12
}That’s the minimum. Everything else is optional and tunes engine behavior.
| Field | Type | Required | Description |
|---|---|---|---|
jurisdictions | string[] | yes | 1–20 jurisdiction labels (e.g. UK, EU, Singapore). Each ≤100 chars. |
areas | string[] | yes | 1–20 topic labels (e.g. AML, Sanctions, Consumer Duty). Each ≤200 chars. |
horizon | int | yes | One of 3, 6, 12, 18, 24, 36 (months). |
engine | string | no | v1 / v2 (default) / v3 / v4 / admiral / v4.5-alpha / v4.5-beta. |
model | string | no | Analyst model. Common values: sonnet (default), opus, opus47, haiku. |
searchModel | string | no | Search-pass model. Same values as model. |
verificationMode | "in-analyst" | null | no | Enables verifier pass (only valid on v4 / v4.5-*). |
realist | bool | no | Run the realist critic (only valid on verifier-aware engines). |
auditor | bool | no | Run the auditor critic (only valid on verifier-aware engines). |
auditorModel | "sonnet" | no | Auditor model — currently only sonnet. |
embeddingsProvider | string | no | Override the embedding provider used for drift detection. |
[!TIP] If you don’t pass
engineormodel, the API uses the same defaults the regsn.app dashboard uses. For most integrations that’s the right choice — track the Changelog for breaking-default changes.
Two execution modes
POST /v1/scans runs in one of two modes, selected by ?mode=.
Sync mode (default)
POST /v1/scans (or POST /v1/scans?mode=sync) blocks for up to 120 seconds. If the engine finishes inside that window you get 200 OK with the full snapshot in the body — no second round-trip needed:
{
"scan_id": "8f3d…",
"status": "completed",
"snapshot_id": "a1b2…",
"snapshot": { "items": [...], "trends": [...], "executive_summary": {...}, "executive_narrative": "...", "_meta": {...} }
}If the scan doesn’t finish in 120 seconds, you get 202 Accepted with a scan_id and the URLs to keep going:
{
"scan_id": "8f3d…",
"status": "running",
"status_url": "/v1/scans/8f3d…",
"stream_url": "/v1/scans/8f3d…/stream",
"message": "Scan exceeded sync ceiling; poll or stream"
}The scan continues running in the background — you don’t need to retry. Poll status_url or subscribe to stream_url.
Async mode
Pass ?mode=async and you skip the 120-second wait entirely. The response is 202 immediately, with the same scan_id / status_url / stream_url payload as the timeout case above. Use async whenever you’re driving scans from a queue, a cron, or any context where holding an HTTP connection open is awkward.
curl https://api.regsn.app/v1/scans?mode=async \
-H "Authorization: Bearer $REGSN_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"jurisdictions":["UK"],"areas":["AML"],"horizon":12}'Polling
curl https://api.regsn.app/v1/scans/$SCAN_ID \
-H "Authorization: Bearer $REGSN_API_KEY"{
"id": "8f3d…",
"status": "running",
"progress": { "phase": "analysis", "percent": 42, "message": "Synthesising items…" },
"snapshot_id": null,
"cost_cents": null,
"created_at": "2026-05-14T09:12:33Z",
"completed_at": null,
"error": null
}Terminal states are completed, failed, and cancelled. Once status === "completed", fetch the snapshot at GET /v1/snapshots/{snapshot_id}.
[!TIP] A reasonable polling cadence is 5 seconds. The SDK defaults (
pollMs: 5000in JS,poll_s: 5in Python) reflect this. Polling faster wastes both your rate-limit budget and our gateway capacity; the scan engine itself runs on its own clock.
Streaming (SSE)
For real-time progress, subscribe to GET /v1/scans/{id}/stream. The endpoint speaks Server-Sent Events; any standard SSE client works.
Events:
progress—{ type: "progress", phase, percent, message }. Emitted on every phase advance.complete—{ type: "complete", data: <snapshot envelope>, snapshotId }. Terminal.error—{ type: "error", error: <message> }. Terminal.
const url = new URL(`https://api.regsn.app/v1/scans/${scanId}/stream`);
const headers = { Authorization: `Bearer ${process.env.REGSN_API_KEY}` };
// Note: browser EventSource cannot set custom headers; use a fetch-based SSE
// reader, or proxy through your backend so the browser hits your origin.
const res = await fetch(url, { headers });
const reader = res.body.getReader();
const decoder = new TextDecoder();
for (;;) {
const { value, done } = await reader.read();
if (done) break;
for (const line of decoder.decode(value).split('\n')) {
if (!line.startsWith('data:')) continue;
const evt = JSON.parse(line.slice(5).trim());
if (evt.type === 'progress') console.log(evt.phase, evt.percent);
if (evt.type === 'complete') { console.log('done', evt.snapshotId); return; }
if (evt.type === 'error') { throw new Error(evt.error); }
}
}The server sends a : heartbeat comment every 30 seconds to keep intermediate proxies from closing the connection. Late subscribers (after the scan has already advanced or completed) get the latest progress event immediately on connect.
[!WARNING] Browsers’ built-in
EventSourcecannot set custom headers, so you can’t authenticate directly to/v1/scans/{id}/streamfrom a browser. Either proxy the SSE through your own backend (which sets the bearer header), or use a fetch-based SSE reader as shown above.
Idempotency
POST /v1/scans accepts an optional Idempotency-Key header (1–255 printable ASCII chars; UUID4 is conventional). The semantics:
- Same key + identical body → returns the cached response, with
Idempotency-Replayed: trueheader. 24-hour replay window. - Same key + different body →
409 idempotency_key_in_use. - Same key, first request still running →
409 idempotency_in_progress.
Both SDKs auto-generate a UUID4 if you don’t pass one. Always send a key when retrying after a network failure — without it, you might silently run (and bill) two scans.
See Idempotency for the full semantics.
Fingerprint determinism
Every scan request carries an implicit “input fingerprint”: the SHA-256 of your canonical request body. This is surfaced as _meta.inputFingerprint on the resulting snapshot. The same input fingerprint always belongs to one scan — even across retries, the engine reuses the previous result rather than re-running.
This is not the same thing as idempotency. Idempotency is a 24-hour replay cache keyed by your header. Fingerprint reuse is a longer-lived “we already did exactly this work” check.
[!TIP] If your integration runs the same scan configuration on a schedule (weekly horizon scans, for example), generate a fresh idempotency key per run (a value like
weekly-uk-aml-2026-W19works). The engine’s fingerprint cache will still kick in across runs where nothing material has changed — you don’t need to fight it.
Budget & cost
Every scan deducts from the same Clerk-tracked budget pool that the regsn.app dashboard uses; there’s no separate API meter. If you run out, POST /v1/scans returns 402 budget_exhausted:
{
"type": "https://api.regsn.app/problems/budget_exhausted",
"title": "Budget exhausted",
"status": 402,
"code": "budget_exhausted",
"detail": "Monthly budget exhausted. Top up to continue.",
"request_id": "req_…"
}Top up in the dashboard, then retry. Estimated and actual costs appear on the snapshot in cost_cents / actual_cost_cents and in _meta.estimatedCost.
Validation errors (422)
The request body is validated server-side with the same rules as the dashboard wizard. Common ones:
| Code | Path | Cause |
|---|---|---|
required | jurisdictions / areas | Missing or empty array. |
invalid | horizon | Not one of 3,6,12,18,24,36. |
invalid | engine | Unknown engine. |
incompatible | verificationMode | Not valid on the chosen engine. |
invalid | model / searchModel | Unknown model id. |
haiku_blocked_on_verifier | model | Haiku 4.5 is not supported on verifier-aware paths (v4 with realist/auditor, v4.5-alpha, v4.5-beta). |
A 422 returns an errors array — see Errors.
Worked example: async + stream
from regsn import RegSn
client = RegSn()
# 1. Fire async.
job = client.scans.create_async(
jurisdictions=["UK", "EU"],
areas=["AML", "Sanctions"],
horizon=12,
engine="v4.5-beta",
verificationMode="in-analyst",
)
scan_id = job["scan_id"]
print(f"queued: {scan_id}")
# 2. Block in your own polling loop (or call client.scans.wait()).
result = client.scans.wait(scan_id)
# 3. wait() routes through GET /v1/snapshots/{id}, so the envelope is nested
# under snapshot.data — see the "Two envelope shapes" note on /api/snapshots.
print(result["snapshot"]["data"]["executive_narrative"][:200])See also
- Snapshots — the data model the scan produces.
- Scans and snapshots (concept) — the same lifecycle from the product side.
- Idempotency — full semantics of
Idempotency-Key. - Rate limits —
POST /v1/scansis 6 / hour / key. - Errors — 402, 409, 422, 429 problem-details.
- Glossary — bearer key, fingerprint, idempotency key, SSE stream.