docs/api-scenarios.mdx
The scenarios API is a PRO-only, job-queued surface on top of the WorldMonitor chokepoint + trade dataset. Callers enqueue a named scenario template against an optional country, then poll a job-id until the worker completes. If no country is supplied, v1 computes across the seeded reporter set only: US, CN, RU, IR, IN, and TW.
| Legacy URL | Canonical URL |
|---|---|
POST /api/scenario/v1/run | POST /api/scenario/v1/run-scenario |
GET /api/scenario/v1/status | GET /api/scenario/v1/get-scenario-status |
GET /api/scenario/v1/templates | GET /api/scenario/v1/list-scenario-templates |
Prefer the canonical URLs in new code — the aliases will retire at the next v1→v2 break (tracked in #3282). </Note>
GET /api/scenario/v1/list-scenario-templatesReturns the catalog of pre-defined scenario templates. Cached public, max-age=3600.
Response — abbreviated example using one of the live shipped templates (server/worldmonitor/supply-chain/v1/scenario-templates.ts):
{
"templates": [
{
"id": "hormuz-tanker-blockade",
"name": "Hormuz Strait Tanker Blockade",
"affectedChokepointIds": ["hormuz_strait"],
"disruptionPct": 100,
"durationDays": 14,
"affectedHs2": ["27", "29"],
"costShockMultiplier": 2.10
}
]
}
Other shipped templates at the time of writing: taiwan-strait-full-closure, suez-bab-simultaneous, panama-drought-50pct, russia-baltic-grain-suspension, us-tariff-escalation-electronics. Use the live /list-scenario-templates response as the source of truth — the set grows over time. affectedHs2: [] on the wire means the scenario affects ALL sectors (the registry's null sentinel, which repeated string cannot carry directly).
POST /api/scenario/v1/run-scenarioEnqueues a job. Returns the assigned jobId the caller must poll.
X-WorldMonitor-Key (env key from WORLDMONITOR_VALID_KEYS, or a user-owned wm_-prefixed key whose owner has the apiAccess entitlement), or (b) a Clerk bearer token whose user has role pro or Dodo entitlement tier ≥ 1. A trusted browser Origin alone is not sufficient — isCallerPremium() in server/_shared/premium-check.ts only counts explicit credentials. Browser calls work because premiumFetch() (src/services/premium-fetch.ts) injects one of the two credential forms on the caller's behalf.ENDPOINT_RATE_POLICIES in server/_shared/rate-limit.ts)> 100 is rejected with 429, so depth 100 can still accept one more job.Request:
{
"scenarioId": "hormuz-tanker-blockade",
"iso2": "US"
}
scenarioId — id from /list-scenario-templates. Required.iso2 — optional ISO-3166-1 alpha-2 (uppercase). Scopes the scenario to one country. Empty string means the worker uses the v1 seeded reporter set: US, CN, RU, IR, IN, and TW.Response (202 Accepted):
{
"jobId": "scenario:1713456789012:a1b2c3d4",
"status": "pending",
"statusUrl": "/api/scenario/v1/get-scenario-status?jobId=scenario%3A1713456789012%3Aa1b2c3d4"
}
statusUrl — server-computed convenience URL. Callers that don't want to hardcode the status path can follow this directly (it URL-encodes the jobId).Location response header — carries the same poll URL as statusUrl, per the standard REST async-job pattern (202 + Location → poll until terminal).Treat any 2xx as enqueue success. The interim guidance to branch on response body shape (response.body.status === "pending") instead of the status code remains valid, and statusUrl is preserved exactly as before.
</Note>
Errors:
| Status | message | Cause |
|---|---|---|
| 400 | Validation failed (violations include scenarioId) | Missing or unknown scenarioId |
| 400 | Validation failed (violations include iso2) | Malformed iso2 |
| 403 | PRO subscription required | Not PRO |
| 405 | — | Method other than POST (enforced by sebuf service-config) |
| 429 | Too many requests | Per-IP 10/min gateway rate limit |
| 429 | Scenario queue is at capacity, please try again later | Pending queue depth is greater than 100 before enqueue |
| 502 | Failed to enqueue scenario job | Redis enqueue failure |
GET /api/scenario/v1/get-scenario-status?jobId=<jobId>Returns the job's current state as written by the worker, or a synthesised pending stub while the job is still queued.
/run-scenarioscenario:{unix-ms}:{8-char-suffix} — strictly validated to guard against path traversalStatus lifecycle:
status | When |
|---|---|
pending | Job enqueued but worker has not picked it up yet. Synthesised by the status handler when no Redis record exists. |
processing | Worker dequeued the job and started computing. |
done | Worker completed successfully; result is populated. |
failed | Worker hit a computation error; error is populated. |
Pending response (200):
{ "status": "pending", "error": "" }
Processing response (200):
{ "status": "processing", "error": "" }
Done response (200) — result carries the worker's computed payload:
{
"status": "done",
"error": "",
"result": {
"affectedChokepointIds": ["hormuz_strait"],
"topImpactCountries": [
{ "iso2": "US", "totalImpact": 150.0, "impactPct": 100 }
],
"template": {
"name": "hormuz_strait",
"disruptionPct": 100,
"durationDays": 14,
"costShockMultiplier": 2.10
}
}
}
In the status payload, template.name is the worker-derived key: physical
scenarios join affected chokepoint ids with +, while tariff-shock scenarios
with no physical chokepoint use tariff_shock. It is not the catalog label.
totalImpact is a relative weighted score, not a currency amount or USD import
value. For physical chokepoint scenarios, the worker computes
exposureScore * (disruptionPct / 100) * costShockMultiplier for each matching
exposure entry, then sums by country. For tariff-shock scenarios with no
affected chokepoint ids, it uses
vulnerabilityIndex * costShockMultiplier. impactPct is each returned
country's share of max(maxReturnedTotalImpact, 1), capped at 100. That
denominator floor means the top returned country can be below 100 when every
returned totalImpact is below 1.
Failed response (200):
{ "status": "failed", "error": "computation_error" }
Poll loop: treat pending and processing as non-terminal; only done and failed are terminal. Both pending and processing can legitimately persist for several seconds under load.
Errors:
| Status | message | Cause |
|---|---|---|
| 400 | Validation failed (violations include jobId) | Missing or malformed jobId |
| 403 | PRO subscription required | Not PRO |
| 405 | — | Method other than GET (enforced by sebuf service-config) |
| 502 | Failed to fetch job status | Redis read failure |