spb/internetpressure
Public
TypeScript 36.3%
Python 31.8%
Go 18%
JavaScript 9.8%
Shell 1.9%
SQL 1.4%
CSS 0.5%
1# InternetPressure public API (v1) — contract23Served by `apps/api` (FastAPI) on `:8352`; the Next app (`apps/web`, `:8351`) reaches it through the edge on the same4origin (`/api/*` → api, `/ingest/*` → api, everything else → web). Browser code therefore always calls relative URLs5(`/api/v1/...`). All timestamps are UTC ISO-8601 with `Z`. All numbers are plain JSON numbers (never strings).6Every response carries `Cache-Control: no-store` unless stated. Unknown scope → `404 {"error":"not_found"}`.78Rate limit (public tier): 300 req/min per IP (internal SSR calls exempt); SSE connections: 4 per IP. `429` with `Retry-After`.910Pressure levels (from `packages/config/pressure.yaml`): `calm ≤10`, `normal ≤25`, `elevated ≤40`, `stressed ≤55`,11`high ≤70`, `severe ≤85`, `extreme ≤100`. Every `level` field is one of these ids; `level_label` is its label.1213Internal status (self-exclusion, §57 of the spec): `internal_status` is `"ok"`, `"degraded"` (too few fresh probes /14BGP stale / stores unhealthy — score is frozen at its last value and `stale: true`) or `"stale"` (engine hasn't run).15The UI must show a visible "instrument degraded" state instead of interpreting a frozen number.1617---1819## GET /api/v1/status20```json21{ "ok": true, "ts": "…", "internal_status": "ok", "engine": { "last_run": "…", "cycle_ms": 412, "cycle_seconds": 10 },22 "ingest": { "last_batch": "…", "batches_5m": 312, "measurements_5m": 41200 },23 "probes": { "fresh": 8, "total": 8, "excluded": [] },24 "bgp": { "fresh": true, "last_message": "…", "collectors": 12 },25 "stores": { "clickhouse": true, "postgres": true, "redis": true }, "version": "0.1.0" }26```2728## GET /api/v1/pressure/global29```json30{31 "ts": "…", "pressure": 42.7, "level": "stressed", "level_label": "Stressed",32 "delta_1h": 6.3, "delta_24h": -2.1, "velocity_per_h": 7.2, "acceleration_per_h2": 3.1, "volatility_1h": 2.4,33 "trend": "rising", // rising | falling | stable34 "confidence": 0.86, "stale": false, "internal_status": "ok",35 "coverage": { "probes_active": 8, "probes_total": 8, "probe_regions": 5, "targets": 212,36 "measurements_5m": 41200, "bgp_collectors": 12, "baseline_days": 6.2 },37 "components": [38 { "id": "routing", "label": "Routing", "score": 57.1, "weight": 0.25, "contribution": 14.3,39 "trend": "rising", "delta_1h": 11.0, "confidence": 0.9,40 "drivers": [ { "label": "BGP withdrawals/s 4.8× baseline", "points": 9.1, "scope_type": "bgp", "scope_id": "withdrawals" } ] },41 { "id": "latency", … }, { "id": "dns", … }, { "id": "availability", … }, { "id": "http_tls", … },42 { "id": "path", … }, { "id": "corroboration", … }43 ],44 "explain": [45 { "text": "+14.3 points from elevated BGP route churn", "points": 14.3, "component": "routing", "scope_type": "global", "scope_id": null },46 { "text": "+11.0 from North America East packet loss", "points": 11.0, "component": "latency", "scope_type": "region", "scope_id": "na-east" },47 { "text": "−3.1 because Western Europe remains stable", "points": -3.1, "component": "latency", "scope_type": "region", "scope_id": "eu-west" }48 ],49 "sparkline_1h": [ 36.1, 36.4, … ] // 60 points, 1 per minute, oldest first (null where missing)50}51```5253## GET /api/v1/pressure/history?scope_type=global|region|country|asn|service|component&scope_id=…&range=1h|6h|24h|7d|30d|1y54Server-side aggregation; step is chosen by the server (`1h→10s`, `6h→1m`, `24h→1m`, `7d→5m`, `30d→1h`, `1y→1d`).55```json56{ "scope_type": "global", "scope_id": null, "range": "24h", "step_seconds": 60,57 "points": [ { "ts": "…", "pressure": 41.2, "components": { "routing": 55.0, "latency": 40.1, "dns": 12.0,58 "availability": 30.3, "http_tls": 22.0, "path": 47.9, "corroboration": 0.0 }, "confidence": 0.85 } ],59 "summary": { "min": 21.0, "max": 61.4, "avg": 33.7, "max_ts": "…" } }60```61`Cache-Control: public, max-age=30` for ranges ≥ 24h.6263## GET /api/v1/pressure/regions64```json65{ "ts": "…", "regions": [66 { "id": "na-east", "name": "North America East", "continent": "North America", "lat": 43, "lon": -76,67 "pressure": 51.3, "level": "stressed", "level_label": "Stressed", "delta_1h": 12.1, "trend": "rising", "confidence": 0.8,68 "components": { "routing": null, "latency": 63.2, "dns": 10.1, "availability": 35.0, "http_tls": 20.0, "path": 58.0 },69 "probes": 3, "targets": 41, "incidents": 1, "coverage_ok": true, "role": "both" } ] } // role: probe | target | both70```71Regional pressure = stress observed **from** probes in the region (source view) blended with stress observed **toward**72targets anchored in the region (destination view); `routing` is null for regions without ASN attribution.7374## GET /api/v1/pressure/region/{id}75Region object above plus: `history_24h` (`{step_seconds, points:[{ts,pressure}]}`), `baseline_7d` (`{median, p90}`),76`incidents` (list, see incidents), `top_asns` (`[{asn, name, pressure}]`), `top_services` (`[{slug, name, pressure, observed_availability_24h}]`),77`probes` (probe list objects), `matrix` (latency matrix rows for this region, see /latency).7879## GET /api/v1/pressure/countries80```json81{ "ts": "…", "countries": [ { "cc": "CA", "name": "Canada", "region": "na-east", "lat": 56.1, "lon": -106.3,82 "pressure": 34.0, "level": "elevated", "level_label": "Elevated", "delta_1h": 2.0, "trend": "stable",83 "components": { … }, "probes": 2, "targets": 18, "role": "both", "coverage_ok": true } ] }84```85Only countries where we have at least one probe or one anchored target appear. The map colours these; every other86country is drawn neutral (we do not pretend to observe it).8788## GET /api/v1/pressure/country/{cc}89Country object plus `history_24h`, `baseline_7d`, `incidents`, `asns`, `services`, `probes`, `targets`90(`[{target_id, name, category, pressure, ok_ratio_1h, ttfb_ms_median}]`).9192## GET /api/v1/asns93`{ "ts": …, "asns": [ { "asn": 13335, "name": "Cloudflare", "country": "US", "pressure": 18.2, "level": "normal",94"routing": 12.0, "latency": 22.1, "availability": 9.0, "targets": 6, "prefixes_observed": 1200, "importance": 5 } ] }`9596## GET /api/v1/pressure/asn/{asn}97```json98{ "asn": 13335, "name": "Cloudflare, Inc.", "country": "US", "importance": 5, "ts": "…",99 "pressure": 18.2, "level": "normal", "level_label": "Normal", "delta_1h": -1.0, "trend": "falling", "confidence": 0.7,100 "components": { "routing": 12.0, "latency": 22.1, "availability": 9.0, "dns": 4.0, "http_tls": 6.0, "path": 15.0 },101 "bgp": { "prefixes_observed_24h": 1200, "announcements_1h": 340, "withdrawals_1h": 12, "churn_ratio": 1.1,102 "origin_changes_1h": 0, "path_stability": 0.97, "series_24h": [ { "ts": "…", "announcements": 5, "withdrawals": 0 } ] },103 "regions_observed": [ "na-east", "eu-west", "eu-east-med" ],104 "targets": [ { "target_id": "cloudflare-www", "name": "Cloudflare", "pressure": 12.0, "ok_ratio_1h": 1.0, "ttfb_ms_median": 71.0 } ],105 "history_24h": { "step_seconds": 60, "points": [ … ] }, "incidents": [ … ] }106```107108## GET /api/v1/services109`{ "services": [ { "slug": "cloudflare", "name": "Cloudflare", "category": "cdn", "pressure": 12.0, "level": "normal",110"observed_availability_24h": 0.9994, "targets": 6, "affected_regions": [], "vendor_status": { "indicator": "none", "incidents": 0, "source": "status.cloudflare.com", "checked_at": "…" } } ] }`111`vendor_status` is null when we have no connector for that provider.112113## GET /api/v1/service/{slug}114Service object plus:115```json116{ "observed": { "availability_24h": 0.9994, "availability_1h": 1.0, "ttfb_ms_median_1h": 71.0, "ttfb_ms_baseline": 68.0,117 "tls_ms_median_1h": 30.1, "failures_1h": 2 },118 "affected_regions": [ { "id": "na-east", "name": "…", "observation": "Elevated TLS latency from 2 probes" } ],119 "vendor_status": { "indicator": "minor", "incidents": 1, "titles": ["…"], "source": "…", "url": "…", "checked_at": "…" },120 "discrepancy": "Vendor reports no incident; we observe elevated TLS latency from 4 probe regions.", // or null121 "matrix": [ { "probe_id": "ca-qc-01", "probe_region": "na-east", "targets": [ { "target_id": "…", "ok": true, "ttfb_ms": 70.0, "z": 0.4, "ts": "…" } ] } ],122 "targets": [ … ], "history_24h": { … }, "incidents": [ … ] }123```124125## GET /api/v1/targets · GET /api/v1/target/{id}126List: `{ "targets": [ { "target_id", "name", "hostname", "category", "provider", "service_id", "country", "region", "importance", "tier", "pressure", "ok_ratio_1h", "ttfb_ms_median_1h" } ] }`.127Detail adds `latest_by_probe` (`[{probe_id, kind, ts, ok, error, dns_ms, tcp_ms, tls_ms, ttfb_ms, http_status, resolved_ip, packet_loss, rtt_avg_ms, z}]`),128`series_24h` (`{step_seconds, points:[{ts, ttfb_ms_p50, ok_ratio}]}`) and `dns` (`{ resolvers: [{resolver, rcode, answers, ms}], disagreement: false }`).129130## GET /api/v1/probes131```json132{ "probes": [ { "probe_id": "ca-qc-01", "name": "Québec City (Bell)", "region": "na-east", "country": "CA", "city": "Québec",133 "provider": "Bell Canada", "asn": 577, "lat": 46.8, "lon": -71.2, "status": "online", // online | stale | offline | excluded134 "last_seen": "…", "version": "0.1.0", "measurements_1h": 5120, "uptime_24h": 0.998, "clock_offset_ms": -14,135 "capabilities": ["http","dns","ping","traceroute"] } ] }136```137138## GET /api/v1/incidents?status=active|resolved|all&limit=50&offset=0139```json140{ "total": 3, "incidents": [ {141 "event_id": "evt_01J…", "slug": "2026-09-12-north-america-east-latency-anomaly",142 "type": "regional_latency", // regional_latency | dns_disruption | routing_instability | service_degradation | availability_loss | path_instability | global_pressure143 "title": "North America East latency anomaly", "summary": "Elevated latency and packet loss observed from 3 probes toward 41 targets.",144 "status": "active", // detected | developing | active | recovering | resolved145 "scope_type": "region", "scope_id": "na-east", "scope_label": "North America East",146 "started_at": "…", "updated_at": "…", "ended_at": null, "duration_s": 1260,147 "peak_pressure": 76.0, "current_pressure": 71.2, "confidence": 0.93,148 "affected_probes": 3, "affected_targets": 41, "affected_asns": [577, 16276], "affected_services": ["aws", "github"],149 "hypotheses": [ { "text": "Possible upstream transit issue", "confidence": 0.6,150 "evidence": ["Route fingerprints changed on 62 % of paths", "Latency rose on paths crossing AS6453"] } ]151} ] }152```153154## GET /api/v1/incident/{slug}155Incident object plus `timeline` (`[{ts, status, pressure, note}]`), `evidence` (`[{signal_id, label, scope_type, scope_id,156current, baseline, robust_z, samples, ts}]`), `series` (`{step_seconds, points:[{ts, pressure, global_pressure}]}` from 30 min157before start to now/end), `probes` (`[{probe_id, region, observation}]`), `targets` (`[{target_id, name, service_id, observation}]`),158`bgp` (`{withdrawals_ratio, announcements_ratio, origin_changes}` or null), `annotations` (`[{ts, author, text}]`).159160## GET /api/v1/fronts161```json162{ "ts": "…", "fronts": [ { "id": "front_na-east_eu-west", "name": "North Atlantic Pressure Front", "status": "developing",163 "intensity": 74.0, "confidence": 0.89, "direction": "east", "since": "…",164 "from": { "region": "na-east", "name": "North America East", "lat": 43, "lon": -76 },165 "to": { "region": "eu-west", "name": "Western Europe", "lat": 49, "lon": 3 },166 "observed": { "latency_pct": 43.0, "churn_x": 4.8, "loss_pct": 3.1, "pairs": 17, "targets": 17, "route_changes": 9 } } ] }167```168169## GET /api/v1/bgp/stats170```json171{ "ts": "…", "fresh": true, "updates_per_s": 812.4, "announcements_per_s": 760.0, "withdrawals_per_s": 52.4,172 "baseline": { "announcements_per_s": 640.0, "withdrawals_per_s": 11.0 }, "ratio": { "announcements": 1.19, "withdrawals": 4.76 },173 "unique_prefixes_1m": 14211, "unique_origins_1m": 2210, "origin_changes_1m": 3, "peers": 1450,174 "collectors": [ { "id": "rrc00", "location": "Amsterdam", "announcements_per_s": 120.1, "withdrawals_per_s": 8.0, "peers": 210, "last_message": "…", "fresh": true } ],175 "series_1h": [ { "ts": "…", "announcements": 7600, "withdrawals": 520 } ], // per minute176 "top_origins_1h": [ { "asn": 13335, "name": "Cloudflare", "announcements": 340, "withdrawals": 12 } ] }177```178179## GET /api/v1/latency180```json181{ "ts": "…", "global": { "rtt_ms_median": 41.2, "rtt_ms_baseline": 39.0, "ttfb_ms_median": 118.0, "ttfb_ms_baseline": 110.0, "packet_loss_pct": 0.3 },182 "matrix": [ { "from": "na-east", "to": "eu-west", "rtt_ms": 92.1, "rtt_ms_baseline": 88.0, "ttfb_ms": 160.0, "loss_pct": 0.0, "z": 0.6, "pairs": 37 } ],183 "by_probe": [ { "probe_id": "ca-qc-01", "rtt_ms_median": 30.1, "ttfb_ms_median": 90.0, "loss_pct": 0.0, "z": 0.2 } ] }184```185186## GET /api/v1/ticker187```json188{ "ts": "…", "bgp_updates_per_s": 812.4, "bgp_withdrawals_per_s": 52.4, "bgp_updates_per_min": 48744,189 "probes_active": 8, "probes_total": 8, "measurements_per_s": 12.3, "measurements_per_min": 738,190 "targets_degraded": 4, "targets_total": 212, "regions_elevated": 3, "regions_normal": 9, "regions_severe": 0,191 "dns_failures_per_min": 2, "median_global_rtt_ms": 41.2, "route_changes_per_min": 1.2, "active_incidents": 1,192 "internal_status": "ok" }193```194195## GET /api/v1/routes?probe={probe_id}&target={target_id}196```json197{ "probe": { "probe_id": "…", "name": "…", "asn": 577 }, "target": { "target_id": "…", "name": "…", "hostname": "…", "asn": 13335 },198 "current": { "ts": "…", "route_hash": "…", "reached": true, "total_ms": 30.2,199 "hops": [ { "n": 1, "ip": "192.168.2.1", "asn": null, "asn_name": null, "rtt_ms": 1.2, "private": true } ] },200 "baseline": { "route_hash": "…", "share_7d": 0.82, "first_seen": "…", "last_seen": "…", "hops": [ … ] },201 "diff": { "changed": true, "added": [ { "n": 6, "ip": "…", "asn": 6453 } ], "removed": [ … ],202 "asn_path_current": [577, 6453, 13335], "asn_path_baseline": [577, 577, 13335], "latency_shift_ms": 12.4, "hop_delta": 1 },203 "history_24h": [ { "ts": "…", "route_hash": "…", "hop_count": 11, "total_ms": 30.2 } ],204 "route_share_7d": [ { "route_hash": "…", "share": 0.82, "asn_path": [ … ] } ] }205```206`GET /api/v1/routes/pairs` → `{ "pairs": [ { "probe_id", "target_id", "changed_24h": 3, "current_route_hash", "stable": false } ] }`.207208## GET /api/v1/history/summary?year=2026&month=9209```json210{ "year": 2026, "month": 9, "days": [ { "date": "2026-09-12", "min": 18.0, "max": 61.4, "avg": 30.2, "events": 2 } ],211 "top_events": [ incident objects, by peak_pressure ], "top_asns": [ { "asn", "name", "events", "max_pressure" } ],212 "top_regions": [ { "id", "name", "events", "max_pressure", "hours_elevated" } ],213 "largest": { "pressure": {…incident}, "routing": {…}, "dns": {…}, "latency": {…} }, "available_months": ["2026-09"] }214```215Without `month` → per-month rows in `months` instead of `days`. Without `year` → all years.216217## GET /api/v1/explain218Deep explainability: `{ "ts", "pressure", "components": [ { "id", "score", "weight", "contribution", "signals": [219{ "signal_id": "ttfb_z", "label": "…", "scope_type": "region", "scope_id": "na-east", "current": 161.0, "baseline_median": 110.0,220 "mad": 9.0, "robust_z": 5.6, "samples": 412, "stress": 0.71, "contribution": 6.2 } ] } ], "excluded_probes": [], "notes": [ "…" ] }`221222## GET /api/v1/methodology223Public copy of the scoring config: `{ "weights": {…}, "levels": [ … ], "engine": { "cycle_seconds", "baseline_days", "z_anomaly", … }, "version": 1, "updated_at": "…" }`.224225## GET /api/v1/search?q=226`{ "results": [ { "type": "country|asn|service|region|target|incident", "id": "…", "label": "…", "href": "/asn/13335", "pressure": 12.0 } ] }`227228## GET /api/v1/live — Server-Sent Events229Headers `Content-Type: text/event-stream`, `Cache-Control: no-store`, `X-Accel-Buffering: no`. Sends `retry: 5000`, an `id:` per230message, a `: ping` comment every 15 s, and on connect an immediate `snapshot` event.231232| event | data |233|---|---|234| `snapshot` | `{ "global": <GET /pressure/global>, "ticker": <GET /ticker>, "regions": <regions[]>, "fronts": <fronts[]>, "incidents": <active incidents[]> }` |235| `global_pressure_update` | same shape as `GET /pressure/global` (sent every engine cycle, i.e. only when the engine actually ran) |236| `regional_pressure_update` | `{ "ts", "regions": [ … ], "countries": [ { "cc", "pressure", "level", "delta_1h" } ] }` |237| `ticker` | `<GET /ticker>` (every 5 s, from real counters) |238| `bgp_stats` | `{ "ts", "updates_per_s", "announcements_per_s", "withdrawals_per_s", "ratio": {…}, "fresh" }` (every 5 s) |239| `probe_stats` | `{ "ts", "probes_active", "probes_total", "measurements_per_s", "excluded": [] }` |240| `incident_created` / `incident_updated` | incident object |241| `service_degradation` | `{ "ts", "slug", "name", "pressure", "regions": [ … ], "observation": "…" }` |242| `front_update` | `<GET /fronts>` |243| `internal_status` | `{ "ts", "internal_status", "reason" }` |244245Nothing is ever emitted without a corresponding backend computation — if the engine pauses, the stream only pings.246247---248249## Admin API (`/api/admin/*`, header `X-IP-Admin-Token: <token>`)250251- `GET /api/admin/overview` → `{ "probes": [ { …probe, "health": { "uptime_24h", "clock_offset_ms", "missing_ratio_1h", "error_rate_1h",252 "buffered", "spool_bytes", "version", "last_health" } } ], "ingest": { "batches_per_min", "measurements_per_min", "rejected_per_min", "last_batch" },253 "stores": { "clickhouse": { "ok", "inserts_per_s", "tables": [ { "name", "rows", "bytes", "oldest", "newest" } ] }, "postgres": { "ok", "size_bytes" }, "redis": { "ok", "used_memory_bytes", "keys" } },254 "bgp": { "collectors": [ … ], "messages_per_s", "fresh", "reconnects_24h" }, "engine": { "last_run", "cycle_ms_p50", "cycle_ms_max", "runs_1h", "errors_1h", "internal_status", "excluded_probes" },255 "corroboration": [ { "id", "name", "ok", "last_fetch", "incidents" } ] }`256- `GET /api/admin/targets` · `POST /api/admin/targets` (target object) · `PATCH /api/admin/targets/{id}` · `DELETE /api/admin/targets/{id}`257- `GET /api/admin/probes` · `POST /api/admin/probes` `{probe_id, name, region, country, city, provider, asn, lat, lon}` → `{ …probe, "key": "<hex, shown once>" }`258 · `PATCH /api/admin/probes/{id}` (`enabled`, metadata) · `POST /api/admin/probes/{id}/rotate-key` → new key259- `GET /api/admin/config` → full `pressure.yaml` as JSON · `PUT /api/admin/config` (validates: weights sum to 1 ± 0.001) → stored in Postgres `config` and applied next cycle260- `GET /api/admin/baselines?signal_id=&scope_type=&scope_id=` → `{ "signal_id", "points": [ { "ts", "value", "median", "mad", "z" } ], "samples", "baseline_days" }`261- `GET /api/admin/raw?table=measurements|traceroutes|bgp_events|bgp_stats|pressure_history|signal_features|probe_health&probe_id=&target_id=&limit=200` → `{ "columns": [ … ], "rows": [ [ … ] ] }`262- `GET /api/admin/incidents?status=` · `PATCH /api/admin/incidents/{id}` `{ "review": "confirmed|dismissed|unreviewed", "note": "…" }`263- `POST /api/admin/annotations` `{ "ts", "scope_type", "scope_id", "text" }` · `GET /api/admin/annotations`264- `POST /api/admin/replay` `{ "from": "…", "to": "…", "weights": {…} }` → `{ "step_seconds", "points": [ { "ts", "pressure_original", "pressure_replayed" } ] }`265- `POST /api/admin/boost` `{ "targets": [ … ], "factor": 0.5, "seconds": 900 }` — manual sampling boost pushed to probes.266267Errors: `401 {"error":"unauthorized"}`, `422 {"error":"validation", "detail": …}`.268