Skip to main content
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.
This service is documented inline (not yet proto-backed). Proto migration is tracked in issue #3207 and will replace this page with auto-generated reference.

List templates

GET /api/scenario/v1/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):
{
  "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 /templates response as the source of truth — the set grows over time.

Run a scenario

POST /api/scenario/v1/run

Enqueues a job. Returns 202 Accepted with a jobId the caller must poll.
  • Auth: PRO entitlement required. Granted by either (a) a valid 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.
  • Rate limits:
    • 10 jobs / minute / user
    • Global queue capped at 100 in-flight jobs; excess rejected with 429 + Retry-After: 30
Request:
{
  "scenarioId": "hormuz-tanker-blockade",
  "iso2": "SG"
}
  • scenarioId — id from /templates. Required.
  • iso2 — optional ISO-3166-1 alpha-2 (uppercase). Scopes the scenario to one country.
Response (202):
{
  "jobId": "scenario:1713456789012:a1b2c3d4",
  "status": "pending",
  "statusUrl": "/api/scenario/v1/status?jobId=scenario:1713456789012:a1b2c3d4"
}
Errors:
StatuserrorCause
400Invalid JSON bodyBody is not valid JSON
400scenarioId is requiredMissing field
400Unknown scenario: ...scenarioId not in the template catalog
400iso2 must be a 2-letter uppercase country codeMalformed iso2
403PRO subscription requiredNot PRO
405Method other than POST
429Rate limit exceeded: 10 scenario jobs per minutePer-user rate limit
429Scenario queue is at capacity, please try again laterGlobal queue > 100
502Failed to enqueue scenario jobRedis enqueue failure
503Service temporarily unavailableMissing env

Poll job status

GET /api/scenario/v1/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
  • jobId format: scenario:{unix-ms}:{8-char-suffix} — strictly validated to guard against path traversal
Status lifecycle:
statusWhenAdditional fields
pendingJob enqueued but worker has not picked it up yet. Synthesised by the status handler when no Redis record exists.
processingWorker dequeued the job and started computing. Written by the worker at job pickup.startedAt (ms epoch)
doneWorker completed successfully.completedAt, result (scenario-specific payload)
failedWorker hit a computation error.failedAt, error (string)
Pending response (200):
{
  "jobId": "scenario:1713456789012:a1b2c3d4",
  "status": "pending"
}
Processing response (200):
{
  "status": "processing",
  "startedAt": 1713456789500
}
Done response (200) — the worker writes the result directly to Redis; the status endpoint returns it verbatim:
{
  "status": "done",
  "completedAt": 1713456890123,
  "result": {
    "costShockPct": 14.2,
    "affectedImportValueUsd": 8400000000,
    "topExposedSectors": ["refined_petroleum", "chemicals"]
  }
}
Failed response (200):
{
  "status": "failed",
  "error": "computation_error",
  "failedAt": 1713456890123
}
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:
StatuserrorCause
400Invalid or missing jobIdMissing or malformed jobId
403PRO subscription requiredNot PRO
405Method other than GET
500Corrupted job resultWorker wrote invalid JSON
502Failed to fetch job statusRedis read failure
503Service temporarily unavailableMissing env

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.