|
| 1 | +--- |
| 2 | +title: "Build a Supply-Chain Early-Warning System in an Afternoon" |
| 3 | +description: "Score your trade lanes for chokepoint exposure, subscribe to disruption webhooks, and pipe alerts to Slack — a working tutorial on the World Monitor API." |
| 4 | +metaTitle: "Supply Chain Risk API Tutorial: Chokepoint Alerts | World Monitor" |
| 5 | +keywords: "supply chain risk API, chokepoint monitoring API, shipping disruption alerts, maritime risk API, trade route risk scoring, supply chain early warning system" |
| 6 | +audience: "Supply chain engineers, logistics developers, procurement analysts, platform teams, risk managers" |
| 7 | +heroImage: "/blog/images/blog/build-supply-chain-early-warning-system-api.jpg" |
| 8 | +pubDate: "2026-06-08" |
| 9 | +--- |
| 10 | + |
| 11 | +When the Strait of Hormuz shut down this spring, companies found out in one of two ways. Some read about it in the news and started calling freight forwarders. Others had already received a webhook hours earlier, when the disruption score crossed their alert threshold, and were quoting Cape of Good Hope routings before their competitors knew there was a problem. |
| 12 | + |
| 13 | +The second group did not have a $50,000-a-year risk platform. The capability they used — [live chokepoint tracking](/blog/posts/tracking-global-trade-routes-chokepoints-freight-costs/) with programmatic alerts — is part of World Monitor's API. This post builds that early-warning system end to end: score your lanes, subscribe to alerts, verify deliveries, and route them to Slack. |
| 14 | + |
| 15 | +You need an API key (`X-WorldMonitor-Key`, issued with [Pro or API plans](https://www.worldmonitor.app/pro)) and about an afternoon. |
| 16 | + |
| 17 | +## The Architecture |
| 18 | + |
| 19 | +Three signals, three endpoints: |
| 20 | + |
| 21 | +1. **Lane exposure** — which chokepoints does each of my trade lanes depend on? (`/api/v2/shipping/route-intelligence`) |
| 22 | +2. **Live disruption** — push notification when a chokepoint's disruption score crosses my threshold (`/api/v2/shipping/webhooks`) |
| 23 | +3. **Country context** — structural resilience of origin and destination countries (`/api/resilience/v1/get-resilience-score`) |
| 24 | + |
| 25 | +A small receiver service glues them together and posts to Slack. |
| 26 | + |
| 27 | +## Step 1: Score Your Lanes |
| 28 | + |
| 29 | +For each lane you ship, ask the route-intelligence endpoint what it crosses and how disrupted that is right now: |
| 30 | + |
| 31 | +```bash |
| 32 | +curl -s 'https://api.worldmonitor.app/api/v2/shipping/route-intelligence?fromIso2=AE&toIso2=NL&cargoType=tanker&hs2=27' \ |
| 33 | + -H 'X-WorldMonitor-Key: wm_YOUR_KEY' |
| 34 | +``` |
| 35 | + |
| 36 | +The response tells you everything a routing decision needs: |
| 37 | + |
| 38 | +```json |
| 39 | +{ |
| 40 | + "primaryRouteId": "ae-to-eu-via-hormuz-suez", |
| 41 | + "chokepointExposures": [ |
| 42 | + { "chokepointId": "hormuz_strait", "chokepointName": "Strait of Hormuz", "exposurePct": 100 }, |
| 43 | + { "chokepointId": "suez", "chokepointName": "Suez Canal", "exposurePct": 100 } |
| 44 | + ], |
| 45 | + "bypassOptions": [ |
| 46 | + { |
| 47 | + "name": "Cape of Good Hope", |
| 48 | + "type": "maritime_detour", |
| 49 | + "addedTransitDays": 12, |
| 50 | + "addedCostMultiplier": 1.35, |
| 51 | + "activationThreshold": "DISRUPTION_SCORE_60" |
| 52 | + } |
| 53 | + ], |
| 54 | + "warRiskTier": "WAR_RISK_TIER_ELEVATED", |
| 55 | + "disruptionScore": 68 |
| 56 | +} |
| 57 | +``` |
| 58 | + |
| 59 | +Read it like this: this tanker lane is 100% exposed to both Hormuz and Suez, the current disruption score on the primary chokepoint is 68/100, and the documented bypass adds 12 transit days at a 1.35× cost multiplier. `cargoType` matters — bypass options are filtered to corridors suitable for your cargo (`container`, `tanker`, `bulk`, or `roro`), and `hs2` lets you scope by commodity chapter. |
| 60 | + |
| 61 | +Run this once for every lane in your network and you have an exposure matrix: which chokepoints, at what percentage, with what fallback. Most teams discover that 70% of their volume funnels through two or three waterways. |
| 62 | + |
| 63 | +## Step 2: Subscribe to Disruption Webhooks |
| 64 | + |
| 65 | +Polling is for prototypes. Register a webhook for the chokepoints your matrix surfaced: |
| 66 | + |
| 67 | +```bash |
| 68 | +curl -s -X POST 'https://api.worldmonitor.app/api/v2/shipping/webhooks' \ |
| 69 | + -H 'X-WorldMonitor-Key: wm_YOUR_KEY' \ |
| 70 | + -H 'Content-Type: application/json' \ |
| 71 | + -d '{ |
| 72 | + "callbackUrl": "https://alerts.yourcompany.com/wm-shipping", |
| 73 | + "chokepointIds": ["hormuz_strait", "suez", "bab_el_mandeb"], |
| 74 | + "alertThreshold": 60 |
| 75 | + }' |
| 76 | +``` |
| 77 | + |
| 78 | +The `201` response returns a `subscriberId` and a one-time `secret` — persist it; the server never shows it again (there is a `rotate-secret` endpoint when you need a new one). Omitting `chokepointIds` subscribes you to all 13 monitored chokepoints. Subscriptions expire after 30 days, so re-register on a monthly cron to keep both the record and the owner index alive. |
| 79 | + |
| 80 | +When a chokepoint's disruption score crosses your threshold, you get: |
| 81 | + |
| 82 | +``` |
| 83 | +POST https://alerts.yourcompany.com/wm-shipping |
| 84 | +X-WM-Signature: sha256=<HMAC-SHA256(body, secret)> |
| 85 | +X-WM-Delivery-Id: <ulid> |
| 86 | +X-WM-Event: chokepoint.disruption |
| 87 | +
|
| 88 | +{ |
| 89 | + "chokepointId": "hormuz_strait", |
| 90 | + "score": 74, |
| 91 | + "alertThreshold": 60, |
| 92 | + "triggeredAt": "2026-06-08T12:03:00Z", |
| 93 | + "reason": "ais_congestion_spike" |
| 94 | +} |
| 95 | +``` |
| 96 | + |
| 97 | +## Step 3: Verify and Route to Slack |
| 98 | + |
| 99 | +Never trust an unverified webhook. The signature is a standard HMAC over the raw body: |
| 100 | + |
| 101 | +```js |
| 102 | +import express from 'express'; |
| 103 | +import { createHmac, timingSafeEqual } from 'node:crypto'; |
| 104 | + |
| 105 | +const app = express(); |
| 106 | + |
| 107 | +// Express does not expose the raw body by default, but HMAC must be |
| 108 | +// computed over the exact bytes that were signed. Capture them here. |
| 109 | +app.use(express.json({ |
| 110 | + verify: (req, _res, buf) => { req.rawBody = buf; }, |
| 111 | +})); |
| 112 | + |
| 113 | +function verify(rawBody, signatureHeader, secret) { |
| 114 | + const expected = createHmac('sha256', secret).update(rawBody).digest('hex'); |
| 115 | + const received = (signatureHeader || '').replace('sha256=', ''); |
| 116 | + return received.length === expected.length && |
| 117 | + timingSafeEqual(Buffer.from(received), Buffer.from(expected)); |
| 118 | +} |
| 119 | + |
| 120 | +// Remembers delivery IDs we have already processed. Use a TTL cache or a |
| 121 | +// shared store (Redis) in production — an unbounded Set leaks memory. |
| 122 | +const seenDeliveries = new Set(); |
| 123 | + |
| 124 | +app.post('/wm-shipping', (req, res) => { |
| 125 | + if (!verify(req.rawBody, req.headers['x-wm-signature'], SECRET)) { |
| 126 | + return res.status(401).end(); |
| 127 | + } |
| 128 | + |
| 129 | + const deliveryId = req.headers['x-wm-delivery-id']; |
| 130 | + if (seenDeliveries.has(deliveryId)) return res.status(200).end(); |
| 131 | + seenDeliveries.add(deliveryId); |
| 132 | + |
| 133 | + const { chokepointId, score, reason } = req.body; |
| 134 | + postToSlack(`:rotating_light: ${chokepointId} disruption at ${score}/100 (${reason}). ` + |
| 135 | + `Affected lanes: ${lanesExposedTo(chokepointId).join(', ')}`); |
| 136 | + res.status(200).end(); |
| 137 | +}); |
| 138 | +``` |
| 139 | + |
| 140 | +Three production notes from the delivery contract: the HMAC must be computed over the **raw request bytes**, which is why the `express.json` `verify` hook stashes `req.rawBody` — recomputing it from the parsed object will not match; delivery is **at-least-once**, so deduplicate on `X-WM-Delivery-Id` (back the Set with a TTL cache or Redis so it does not grow forever); and repeated delivery failures deactivate the subscription, so wire up the `reactivate` endpoint in your runbook. |
| 141 | + |
| 142 | +The `lanesExposedTo()` lookup is your exposure matrix from Step 1 — that is what turns a generic "Hormuz is disrupted" alert into "your AE→NL tanker lane just lost its primary route; the Cape bypass costs 1.35× and 12 extra days." |
| 143 | + |
| 144 | +## Step 4: Add Country Context |
| 145 | + |
| 146 | +Chokepoints are not the only failure mode. A supplier country sliding into instability disrupts production before anything reaches a port. Pull structural resilience for your origin countries: |
| 147 | + |
| 148 | +```bash |
| 149 | +curl -s 'https://api.worldmonitor.app/api/resilience/v1/get-resilience-score?countryCode=EG' \ |
| 150 | + -H 'X-WorldMonitor-Key: wm_YOUR_KEY' |
| 151 | +``` |
| 152 | + |
| 153 | +You get a 0–100 resilience score with per-domain breakdowns (energy, infrastructure, governance, security and more), a trend, and a 30-day change — computed across 196 countries and refreshed every six hours. Combine it with the real-time [Country Instability Index](/blog/posts/country-instability-index-methodology-explained/) and you cover both clocks: CII for what is burning this week, resilience for which countries absorb shocks and which shatter. |
| 154 | + |
| 155 | +A simple weekly job that flags any origin country whose resilience dropped more than a few points in 30 days catches slow-burn deterioration that no chokepoint webhook will ever see. |
| 156 | + |
| 157 | +## What You End Up With |
| 158 | + |
| 159 | +- An **exposure matrix** mapping every lane to its chokepoints, bypasses, and current disruption scores |
| 160 | +- **Push alerts** within minutes of a disruption-score threshold crossing, signed and deduplicated |
| 161 | +- **Country-level early warning** on supplier fragility, refreshed every six hours |
| 162 | +- A Slack channel that occasionally says something genuinely important |
| 163 | + |
| 164 | +Total code: one webhook receiver and two cron jobs. If you want to stress-test the design, the [scenario engine](https://www.worldmonitor.app/docs/scenario-engine) simulates events like a Taiwan Strait closure or a Panama drought against live trade data — and AI agents can run the same checks conversationally through the [MCP server](/blog/posts/worldmonitor-mcp-server-ai-agents-real-time-intelligence/). |
| 165 | + |
| 166 | +## Frequently Asked Questions |
| 167 | + |
| 168 | +**Which chokepoints can I monitor?** |
| 169 | + |
| 170 | +All 13 strategic waterways World Monitor tracks, including the Strait of Hormuz, Suez Canal, Bab el-Mandeb, Strait of Malacca, Panama Canal, Taiwan Strait, Bosporus, Kerch Strait, and the Cape of Good Hope bypass corridor. |
| 171 | + |
| 172 | +**How fresh is the disruption data?** |
| 173 | + |
| 174 | +Chokepoint transit counts blend IMF PortWatch weekly baselines with real-time AIS crossing counters; disruption scores update continuously and route-intelligence responses are cached for at most 60 seconds. |
| 175 | + |
| 176 | +**Do I need to host my own receiver?** |
| 177 | + |
| 178 | +For webhooks, yes — any HTTPS endpoint works (private and loopback addresses are rejected at registration). If you just want notifications without code, Pro accounts can route alerts to Slack, Discord, Telegram, or email through the built-in notification channels instead. |
| 179 | + |
| 180 | +--- |
| 181 | + |
| 182 | +**Get an API key at [worldmonitor.app/pro](https://www.worldmonitor.app/pro), pull the full OpenAPI spec from [worldmonitor.app/openapi.yaml](https://www.worldmonitor.app/openapi.yaml), and ship the early-warning system your freight forwarder thinks you bought from someone expensive.** |
0 commit comments