Scans
A scan is a run of the research engine over a set of jurisdictions and regulatory areas. It produces a snapshot. See scans and snapshots for the product-side view.
| Endpoint | Purpose |
|---|---|
POST /v1/scans | Run a scan (sync by default, ?mode=async to queue) |
POST /v1/scans/estimate | Cost envelope for a config, without running it |
GET /v1/scans/{id} | Status and progress |
GET /v1/scans/{id}/stream | Server-Sent Events progress stream |
POST /v1/scans/{id}/cancel | Best-effort cancellation |
Create a scan
POST /v1/scans — bearer auth; rate-limited to 6 scans per hour per key; accepts Idempotency-Key.
Query parameters
| Parameter | Values | Default | Effect |
|---|---|---|---|
mode | sync, async | sync | sync blocks up to 120 seconds and returns the snapshot inline if the engine finishes in time; otherwise it returns 202 and the scan continues in the background. async returns 202 immediately. |
Request body
| Field | Type | Required | Constraints |
|---|---|---|---|
jurisdictions | string[] | yes | 1–20 entries, each under 100 characters. Free text; GET /v1/meta/jurisdictions lists the vocabulary the product uses. |
areas | string[] | yes | 1–20 entries, each under 200 characters. Free text; vocabulary at GET /v1/meta/areas. |
horizon | integer | yes | Months of forward horizon: one of 3, 6, 12, 18, 24, 36. |
engine | string | no | v4 (default), v4.5-alpha, or v4.5-beta. Retired ids (v1, v2, v3, admiral) return 422 with field code engine_deprecated. |
model | string | no | Model id from GET /v1/meta/models, or one of the shorthand ids opus, opus48, sonnet, haiku. haiku is rejected on verifier paths (see below). |
searchModel | string | no | Model for the search pass. Same vocabulary as model. |
verificationMode | string | no | Only value: in-analyst. Valid on any engine listed above. Setting it defaults realist and auditor to true if you left them unset. |
realist | boolean | no | Enables the realist verification pass. |
auditor | boolean | no | Enables the auditor verification pass. |
auditorModel | string | no | Only value: sonnet. |
fetchProvider | string | no | Page-fetch backend for v4.5-beta only: firecrawl (default) or crw. On any other engine returns 422 with field code incompatible. |
translationLanguages | string[] | no | Language codes to translate the snapshot’s narrative content into: fr, de, es, it, zh, ja, pt. Not validated at request time — an unsupported code simply produces no translation. Translations surface via /executive-narrative?language= and export targetLanguage options. |
Constraint interactions, exactly as enforced:
verificationMode,realist: true, andauditor: truerequire an explicitengineofv4,v4.5-alpha, orv4.5-betain the same body (field codeincompatibleotherwise). Omittingengineand relying on thev4default does not satisfy this check — set it explicitly when using verification flags.model: "haiku"is rejected (field codehaiku_blocked_on_verifier) when the config is verifier-aware: enginev4.5-alphaorv4.5-beta, or enginev4withrealistorauditorsettrue.- Jurisdiction and area names are not membership-checked — only count and length. Unrecognised names still run; the meta lists are the vocabulary the product itself scans with.
Example — sync
curl https://api.regsn.app/v1/scans \
-H "Authorization: Bearer $REGSN_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"jurisdictions": ["UK", "EU"],
"areas": ["AML / KYC", "Sanctions"],
"horizon": 12
}'200 — completed within the 120-second ceiling:
{
"scan_id": "8f3d…",
"status": "completed",
"snapshot": { "items": ["…"], "trends": ["…"], "executive_summary": {}, "_meta": {} },
"snapshot_id": "a1b2…"
}202 — ceiling exceeded; the scan continues in the background:
{
"scan_id": "8f3d…",
"status": "running",
"status_url": "/v1/scans/8f3d…",
"stream_url": "/v1/scans/8f3d…/stream",
"message": "Scan exceeded sync ceiling; poll or stream"
}Example — async with verification
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": ["US"],
"areas": ["Capital Markets"],
"horizon": 6,
"engine": "v4.5-alpha",
"model": "sonnet",
"verificationMode": "in-analyst"
}'Returns 202 immediately with the same status_url / stream_url shape as above.
Errors
Status · code | When |
|---|---|
402 budget_exhausted | Account budget pool depleted. Check GET /v1/usage/budget first. |
409 idempotency_key_in_use / idempotency_in_progress | Replayed Idempotency-Key with a different body / original still running. |
422 validation_error | Per-field reasons in errors[] (codes required, invalid, incompatible, engine_deprecated, haiku_blocked_on_verifier). |
429 rate_limit_exceeded | The scans bucket is exhausted (6/hour). Honour Retry-After. |
503 engine_unavailable | Selected engine not loaded in this deployment. |
Estimate a scan
POST /v1/scans/estimate — bearer auth; counts against the reads bucket (60/min), not the scans bucket. Returns a cost envelope without queueing anything and without touching your budget.
The estimator buckets on counts of jurisdictions and areas plus horizon and engine — the names themselves are not consulted, so stub strings give the same answer as real ones.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
jurisdictions | string[] | yes | Non-empty. |
areas | string[] | yes | Non-empty. |
horizon | integer | no | If present, one of 3, 6, 12, 18, 24, 36. (Unlike scan creation, optional here.) |
engine | string | no | Same vocabulary and deprecation rules as scan creation. |
seekerProvider / analystProvider / translatorProvider | string | no | Provider hints; keys as listed by GET /v1/meta/models. |
curl https://api.regsn.app/v1/scans/estimate \
-H "Authorization: Bearer $REGSN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"jurisdictions": ["UK", "EU"],
"areas": ["AML / KYC", "Sanctions"],
"horizon": 12
}'200:
{
"low_cents": 38,
"expected_cents": 55,
"high_cents": 91,
"confidence": "high",
"tier": 1,
"sample_size": 14,
"fingerprint": {
"scan_mode": "standard",
"engine_version": "v4",
"jurisdiction_bucket": "1-2",
"area_bucket": "1-2",
"enabled_exports_count": 0,
"provider_mix_hash": "da39a3ee",
"date_range_bucket": "medium"
},
"estimator_version": "v2.0",
"engine": "v4"
}| Field | Meaning |
|---|---|
low_cents / expected_cents / high_cents | Cost envelope in US cents. All null on a cold start. |
confidence | high, medium, low, or cold_start. |
tier | 1 exact-match window, 2 relaxed one dimension, 3 global similarity-weighted mean; null on cold start. |
sample_size | Historical runs behind the estimate. |
fingerprint | Structured fingerprint of the config; stable across equivalent configs. Absent on cold start. |
estimator_version, engine | Estimator version and the engine the estimate assumed (v4 if you omitted one). |
Errors: 422 validation_error (same field rules as above).
Poll a scan
GET /v1/scans/{id} — bearer auth; reads bucket (60/min).
curl https://api.regsn.app/v1/scans/8f3d… \
-H "Authorization: Bearer $REGSN_API_KEY"200:
{
"id": "8f3d…",
"status": "running",
"progress": { "phase": "research", "percent": 62, "message": "…" },
"snapshot_id": null,
"created_at": "2026-07-20T09:12:03.000Z",
"completed_at": null,
"error": null,
"cost_cents": null
}| Field | Notes |
|---|---|
status | running, completed, failed, or cancelled. |
progress | { phase, percent, message }, updated as the scan advances. |
snapshot_id | Set once status is completed — fetch it via GET /v1/snapshots/{id}. |
error | Failure message when status is failed; otherwise null. |
cost_cents | Actual cost once known, else the pre-scan estimate, else null. |
404 not_found for an id that does not exist or belongs to another account.
Stream scan progress (SSE)
GET /v1/scans/{id}/stream — bearer auth; not rate-limited; Content-Type: text/event-stream.
curl -N https://api.regsn.app/v1/scans/8f3d…/stream \
-H "Authorization: Bearer $REGSN_API_KEY"Every message is a data: line carrying a JSON object whose type field discriminates it (the stream does not use named SSE event: fields — dispatch on type in onmessage):
type | Payload | Terminal |
|---|---|---|
progress | { "type": "progress", "phase": "…", "percent": 62, "message": "…" } | no |
complete | { "type": "complete", "data": <snapshot envelope>, "snapshotId": "a1b2…" } | yes |
error | { "type": "error", "error": "…" } | yes |
cancelled | { "type": "cancelled" } | yes |
Behaviour:
- On subscribe you immediately receive the current
progressstate, so late subscribers are never blind. - If the scan has already finished, the terminal event (
complete,error, or the last stored progress) is emitted once and the connection closes. - A comment line
: heartbeatis sent every 30 seconds to keep proxies from idling the connection out.
data: {"type":"progress","phase":"research","percent":35,"message":"…"}
: heartbeat
data: {"type":"complete","data":{…},"snapshotId":"a1b2…"}404 not_found for an id that does not exist or belongs to another account.
Cancel a scan
POST /v1/scans/{id}/cancel — bearer auth; mutations bucket (30/min).
curl -X POST https://api.regsn.app/v1/scans/8f3d…/cancel \
-H "Authorization: Bearer $REGSN_API_KEY"200:
{ "id": "8f3d…", "status": "cancelled" }Cancellation is best-effort: the job is marked cancelled and stream subscribers receive { "type": "cancelled" }, but engine work already in flight runs to completion server-side — its result is discarded rather than written over the cancelled state.
Status · code | When |
|---|---|
404 not_found | The id does not exist or belongs to another account. |
409 invalid_state | Scan already completed, failed, or cancelled. |
Scheduled, recurring scans are a product feature configured in the dashboard — see the product guide. The API runs one scan per request; schedule on your side with cron or an orchestrator, using an Idempotency-Key per tick. The app’s discounted batch lane is likewise product-only — it is not exposed on /v1.