US, CN, RU, IR, IN, and TW.
This service is proto-backed and included in the published OpenAPI bundle — see
proto/worldmonitor/scenario/v1/service.proto and /api/ScenarioService.openapi.yaml. This page adds migration notes and examples on top of the generated reference.Legacy v1 URL aliases — the sebuf migration (#3207) renamed the three v1 endpoints to align with the proto RPC names. The old URLs are preserved as thin aliases so existing integrations keep working:
Prefer the canonical URLs in new code — the aliases will retire at the next v1→v2 break (tracked in #3282).
List templates
GET /api/scenario/v1/list-scenario-templates
Returns 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):
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).
Run a scenario
POST /api/scenario/v1/run-scenario
Enqueues a job. Returns the assigned jobId the caller must poll.
- Auth: PRO entitlement required. Granted by either (a) a valid
X-WorldMonitor-Key(env key fromWORLDMONITOR_VALID_KEYS, or a user-ownedwm_-prefixed key whose owner has theapiAccessentitlement), or (b) a Clerk bearer token whose user has roleproor Dodo entitlement tier ≥ 1. A trusted browser Origin alone is not sufficient —isCallerPremium()inserver/_shared/premium-check.tsonly counts explicit credentials. Browser calls work becausepremiumFetch()(src/services/premium-fetch.ts) injects one of the two credential forms on the caller’s behalf. - Rate limits:
- 10 jobs / minute / IP (enforced at the gateway via
ENDPOINT_RATE_POLICIESinserver/_shared/rate-limit.ts) - Queue backpressure checks the pending Redis list before enqueue; depth
> 100is rejected with429, so depth100can still accept one more job.
- 10 jobs / minute / IP (enforced at the gateway via
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, andTW.
202 Accepted):
statusUrl— server-computed convenience URL. Callers that don’t want to hardcode the status path can follow this directly (it URL-encodes thejobId).Locationresponse header — carries the same poll URL asstatusUrl, per the standard REST async-job pattern (202+Location→ poll until terminal).
Status-code history (v1 → v1 → v1) — the pre-sebuf-migration endpoint returned
202 Accepted on successful enqueue; the sebuf migration shifted it to 200 OK (no per-RPC status-code configuration exists in sebuf’s HTTP annotations). The original 202 Accepted contract has since been restored — the gateway upgrades the generated 200 via a status-override side-channel and adds the Location header.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.Poll job status
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.
- Auth: same as
/run-scenario - jobId format:
scenario:{unix-ms}:{8-char-suffix}— strictly validated to guard against path traversal
Pending response (
200):
200):
200) — result carries the worker’s computed 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):
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:
Polling strategy
- First poll: ~1s after enqueue.
- Subsequent polls: exponential backoff (1s → 2s → 4s, cap 10s).
- Workers typically complete in 5-30 seconds depending on scenario complexity.
- If still pending after 2 minutes, the job is probably dead — re-enqueue.
