TypeScript 55.4%
Python 43.2%
SQL 1.2%
1import type { Metadata } from "next";2import { Code } from "@/components/code";3import { Chip, PageHeader, Panel, Table, Td } from "@/components/ui";4import { SITE_URL } from "@/lib/api";56export const metadata: Metadata = { title: "API", description: "WebSensor REST API, RSS feed and replayable WebSocket live stream (protocol 2). Public, no key required in phase 1; owner-scoped tools use an anonymous header." };78const PUBLIC: [string, string][] = [9 ["GET /api/v1/events", "List events (cursor pagination). See filters below. order=recent|importance|signal"],10 ["GET /api/v1/events/count", "Count of events matching the same filters as /events"],11 ["GET /api/v1/events/{id|slug}", "Event detail: change + field changes + unified diff, score reasons, related, cluster with propagation, interpretations, snapshots (has_raw), sensor reliability, history of the URL"],12 ["GET /api/v1/changes/{id}", "Raw change with heuristic, semantic class, field changes and the unified patch"],13 ["GET /api/v1/snapshots/{id}", "Snapshot metadata + canonical content; ?raw=1 streams the original body when retention kept it"],14 ["GET /api/v1/snapshots/compare?a=&b=", "Diff between any two snapshots of the same URL"],15 ["GET /api/v1/breaking", "Breaking desk: breaking_now, developing, recently_confirmed clusters + high-signal watching list"],16 ["GET /api/v1/pulse", "Pulse: 6 h activity series, desks, rising entities, anomalous sources, silent changes, group totals"],17 ["GET /api/v1/radar", "Radar: indicators (unusual source activity, silent clusters, doc/repo bursts, status changes, new coverage)"],18 ["GET /api/v1/clusters?limit=&since=", "Event clusters (state, signals, sources, lead time)"],19 ["GET /api/v1/clusters/{id|slug}", "Cluster detail: events, entities, propagation timeline, first-party vs external signals, lead_time_ms"],20 ["GET /api/v1/entities", "Entities (type, q)"],21 ["GET /api/v1/entities/rank?limit=", "Entity ranking: 24 h / 7 d activity, avg signal, confirmed ratio, baseline, rank score"],22 ["GET /api/v1/entities/{id}", "Entity detail: children, relations, sources, aliases, recent, insights (heatmap, anomaly, rank)"],23 ["GET /api/v1/entities/{id}/timeline", "Entity timeline (cursor pagination)"],24 ["GET /api/v1/countries", "Countries with sources, events and breaking counts (24 h)"],25 ["GET /api/v1/countries/{code}", "Country desk: breaking, by category, sources, by type, silent, recent"],26 ["GET /api/v1/categories/{channel}", "Category desk (ai · cyber · finance · health · government · science · products · infrastructure · news)"],27 ["GET /api/v1/sources", "Monitored organizations (category, q, country, tier, first_party)"],28 ["GET /api/v1/sources/{id}", "Source detail: sensors, entities, activity anomaly, quality, discovery, daily"],29 ["GET /api/v1/sensors/{id}", "Sensor detail: runs, snapshots, changes, events"],30 ["GET /api/v1/sensors/{id}/snapshots?limit=", "Snapshot history of one sensor (hashes, HTTP status, has_raw, event slug)"],31 ["GET /api/v1/domains/{domain}/timeline", "Domain: monitored URLs + events"],32 ["GET /api/v1/urls/history?url=", "URL history: snapshots, changes, events, removals"],33 ["GET /api/v1/search?q=", "Search events, entities, sources, URLs, clusters — accepts the q syntax below"],34 ["GET /api/v1/stats", "Platform counters (checks/min, events/min, breaking_now, 304 ratio…)"],35 ["GET /api/v1/trending?hours=24", "Trending entities"],36 ["GET /api/v1/explore", "Explore aggregates"],37 ["GET /api/v1/health/connectors", "Connector health, throughput, engine heartbeat, failing domains, slowest sensors"],38 ["GET /api/v1/feed.rss", "RSS 2.0 of the latest events (same filters as /events)"],39 ["WSS /api/v1/live", "Real-time event stream, protocol 2 (replayable)"],40];4142const OWNER: [string, string][] = [43 ["GET/POST/PUT/DELETE /api/v1/watchlists", "Watchlists; item kinds entity · source · keyword · category · url · event_type · country · group. GET /watchlists/{id}/events"],44 ["GET/POST/PATCH/DELETE /api/v1/alerts", "Alert rules {name, rule, channel: web|webhook, channel_config: {url, secret}}. PATCH {enabled, name}. Webhooks: https only, public hosts only"],45 ["GET /api/v1/notifications?limit=&unread=1", "Deliveries of your rules with event summary; POST /notifications/read {ids?} marks read (all when omitted)"],46 ["GET/POST/DELETE /api/v1/bookmarks", "Saved events {event_id, note?}; GET /bookmarks/ids for a quick membership check"],47 ["GET/POST/DELETE /api/v1/views", "Saved live-feed views {name, query} where query is a /live query string"],48 ["GET/POST/DELETE /api/v1/monitors", "Custom URL monitors {url, name?, frequency: hourly|daily, sensitivity: low|normal|high, selector?, keywords?}; GET /monitors/{id}/events. 5 per owner"],49];5051const FILTERS: [string, string][] = [52 ["after · before", "ISO-8601 bounds on detected_at"],53 ["category · group", "Category slug (ai, cyber…) · event group (security, reliability, product, commercial, corporate, government, science, transport, sports, web)"],54 ["event_type", "Comma list of event types"],55 ["entity · source · domain · sensor · cluster", "Scope to one id"],56 ["importance_min · confidence_min · signal_min", "Score floors (0–100)"],57 ["silent_change · first_party · confirmed", "Booleans (true)"],58 ["country · language · change_class", "ISO-2 country (CA, US, EU…) · language code · semantic class (pricing, policy, product, personnel, meaningful…)"],59 ["q", "Free text + search syntax (right)"],60 ["order · limit · cursor", "recent (default) | importance | signal · ≤ 200 · opaque nextCursor"],61];6263export default function ApiPage() {64 const base = SITE_URL;65 const wss = base.replace(/^http/, "ws");66 return (67 <>68 <PageHeader compact kicker="Machine-readable WebSensor" title="API" description="WebSensor practises what it monitors: stable URLs, JSON, RSS, JSON-LD and a replayable WebSocket feed. Phase 1 is public and unauthenticated; rate limit 600 requests / minute / IP. Owner-scoped tools (watchlists, alerts, bookmarks, views, monitors) use an anonymous X-WebSensor-Owner token generated by your browser. Higher limits, keys or questions: contact@spboucher.ai." />69 <div className="grid grid-cols-1 gap-4 lg:grid-cols-[minmax(0,1fr)_400px]">70 <div className="flex min-w-0 flex-col gap-4">71 <Panel title="Public REST endpoints" dense>72 <Table head={["Endpoint", "Description"]}>73 {PUBLIC.map(([ep, desc]) => (74 <tr key={ep}>75 <Td mono className="sm:whitespace-nowrap">{ep}</Td>76 <Td className="text-fg-muted">{desc}</Td>77 </tr>78 ))}79 </Table>80 </Panel>81 <Panel title={<span>Owner-scoped endpoints <span className="normal-case tracking-normal text-fg-subtle">· header X-WebSensor-Owner: <16–80 chars [A-Za-z0-9_-]></span></span>} dense>82 <Table head={["Endpoint", "Description"]}>83 {OWNER.map(([ep, desc]) => (84 <tr key={ep}>85 <Td mono className="sm:whitespace-nowrap">{ep}</Td>86 <Td className="text-fg-muted">{desc}</Td>87 </tr>88 ))}89 </Table>90 <p className="border-t border-line px-3 py-2 text-[11.5px] text-fg-subtle">No accounts yet: the token is your identity. Anyone holding it can read and edit these resources. Webhook deliveries are signed when a secret is set: <code>X-WebSensor-Signature: sha256=<HMAC-SHA256(secret, raw body)></code>.</p>91 </Panel>92 <Panel title="Event filters · /events, /events/count, /feed.rss" dense>93 <Table head={["Parameter", "Meaning"]}>94 {FILTERS.map(([k, v]) => (95 <tr key={k}>96 <Td mono className="sm:whitespace-nowrap">{k}</Td>97 <Td className="text-fg-muted">{v}</Td>98 </tr>99 ))}100 </Table>101 </Panel>102 <Panel title="Examples">103 <div className="flex flex-col gap-3 text-[13px]">104 <p>Strongest signals of the last 48 h, first-party only:</p>105 <Code>{`curl "${base}/api/v1/events?signal_min=80&first_party=true&order=signal&after=$(date -u -v-48H +%FT%TZ)"`}</Code>106 <p>Silent pricing / terms changes in the commercial group:</p>107 <Code>{`curl "${base}/api/v1/events?silent_change=true&group=commercial&change_class=pricing"`}</Code>108 <p>Everything from Canada in the government category, counted:</p>109 <Code>{`curl "${base}/api/v1/events/count?country=CA&category=government"`}</Code>110 <p>Search syntax (same as the ⌘K palette):</p>111 <Code>{`curl "${base}/api/v1/search?q=$(printf %s 'openai pricing type:pricing_change signal:>70 after:2026-09-01' | jq -sRr @uri)"`}</Code>112 <p>Cluster with propagation timeline and lead time:</p>113 <Code>{`curl "${base}/api/v1/clusters/<slug>" | jq '{lead_time_ms, first_party_signals, external_signals, propagation: .propagation[:3]}'`}</Code>114 <p>Create a signed webhook alert (owner token = any 16–80 char string you keep):</p>115 <Code>{`curl -X POST "${base}/api/v1/alerts" \\116 -H "content-type: application/json" -H "X-WebSensor-Owner: $OWNER" \\117 -d '{"name":"Critical security","rule":{"groups":["security"],"signal_min":80,"first_party_only":true},118 "channel":"webhook","channel_config":{"url":"https://example.com/hooks/ws","secret":"…"}}'`}</Code>119 <p>Subscribe an RSS reader:</p>120 <Code>{`${base}/api/v1/feed.rss?signal_min=60&group=reliability`}</Code>121 </div>122 </Panel>123 <Panel title="Event object">124 <Code lang="json">{`{125 "id": "evt_…", "slug": "openai-api-pricing-changed-…",126 "event_type": "pricing_change", "change_class": "pricing",127 "title": "OpenAI: price changed $10 / million tokens → $8 / million tokens",128 "summary": "…", "why_it_matters": "…",129 "signal_score": 91, "importance": 88.2, "confidence": 98.1, "novelty": 87.4,130 "impact_score": 85, "velocity_score": 40, "anomaly_score": 12,131 "score_reasons": [{ "sign": "+", "text": "First-party source", "points": 8 }, { "sign": "-", "text": "Single source so far", "points": -5 }],132 "importance_components": { "severity": 82, "source": 92, "entity": 92, "novelty": 87, "magnitude": 40, "confirmation": 0, "userImpact": 85, "unusualness": 30 },133 "field_changes": [{ "label": "gpt-4o input", "kind": "price", "before": "$10.00", "after": "$8.00", "deltaPct": -20 }],134 "categories": ["ai", "technology"], "keywords": ["pricing"],135 "silent_change": true, "first_party": true, "country": "US", "language": "en",136 "evidence_label": "OBSERVED",137 "url": "https://openai.com/api/pricing/",138 "published_at": null, "observed_from": "…", "detected_at": "…", "processed_at": "…",139 "detection_latency_ms": null, "processing_latency_ms": 412,140 "cluster": { "id": "clu_…", "slug": "openai-pricing-…", "state": "developing", "event_count": 3, "source_count": 2,141 "first_party_count": 1, "external_count": 2, "velocity": 61, "lead_time_ms": 5400000 },142 "change_id": "chg_…", "old_snapshot_id": "snap_…", "new_snapshot_id": "snap_…",143 "source": { "id": "openai", "name": "OpenAI", "domain": "openai.com", "tier": "S" },144 "sensor": { "id": "openai_pricing", "name": "pricing", "type": "HTML", "connector": "http" },145 "entities": [{ "id": "org_openai", "name": "OpenAI", "type": "organization", "role": "subject" }]146}`}</Code>147 </Panel>148 </div>149 <aside className="flex min-w-0 flex-col gap-4">150 <Panel title="WebSocket live stream · protocol 2">151 <div className="flex flex-col gap-3 text-[13px]">152 <Code>{`wscat -c ${wss}/api/v1/live153> {"subscribe":["group:security","country:CA","entity:org_openai"]}154> {"since":"1757333722000-0"} # after a reconnect`}</Code>155 <p className="text-fg-muted">Channels:</p>156 <ul className="list-disc space-y-0.5 pl-5 font-mono text-[12px] text-fg-muted [overflow-wrap:anywhere]">157 <li>events:global (default) · events:breaking · events:silent · events:first-party</li>158 <li>events:{"{ai|cyber|finance|health|government|science|products|infrastructure|news}"}</li>159 <li>group:{"{security|reliability|product|commercial|corporate|government|science|transport|sports|web}"}</li>160 <li>country:{"{CA}"} · state:{"{breaking|developing|confirmed}"} · type:{"{event_type}"}</li>161 <li>entity:{"{entity_id}"} · source:{"{source_id}"} · watchlist:{"{watchlist_id}"}</li>162 </ul>163 <p className="text-fg-muted">Frames:</p>164 <Code lang="json">{`{"type":"hello","channels":["events:global"]}165{"type":"event","sid":"1757333722000-0","channels":["events:global","group:product"],166 "event":{"id":"evt_…","slug":"…","type":"model_release","title":"…",167 "signal":93,"importance":97,"confidence":95,"novelty":90,"impact":80,"velocity":55,168 "silent":false,"firstParty":true,"country":"US","changeClass":"product",169 "fieldChanges":[…],"evidence":"OBSERVED","group":"product",170 "source":{"id":"openai","name":"OpenAI","domain":"openai.com"},171 "sensor":{"id":"openai_news","name":"news feed","type":"RSS"},172 "entities":[{"id":"org_openai","name":"OpenAI","type":"organization"}],173 "categories":["ai"],"url":"https://…","clusterId":"clu_…","clusterSlug":"…","clusterState":"breaking",174 "detectedAt":"2026-09-08T12:15:22Z","publishedAt":"2026-09-08T12:14:55Z"}}175{"type":"replay_done","since":"1757333722000-0","count":12,"truncated":false}176{"type":"heartbeat","t":1757333722000}`}</Code>177 <ul className="list-disc space-y-1 pl-5 text-[12.5px] text-fg-muted">178 <li>Every event frame carries a stream id <code>sid</code>. Keep the last one; after reconnecting send <code>{`{"since": sid}`}</code> and up to 500 missed events are replayed in order, then <code>replay_done</code>.</li>179 <li>Send <code>{`{"ping":1}`}</code> for a <code>pong</code>. Heartbeats every 25 s keep proxies alive.</li>180 <li>Subscriptions are additive; <code>unsubscribe</code> removes channels (including the default <code>events:global</code>).</li>181 </ul>182 </div>183 </Panel>184 <Panel title="Search syntax · q">185 <Code>{`openai pricing entity:org_openai type:pricing_change,terms_change186after:2026-09-01 before:2026-09-10 after:7d187silent:true first_party:true confirmed:true188importance:>70 confidence:>=50 signal:>80189source:"bank of canada" category:ai country:CA lang:fr190class:pricing group:security cluster:clu_…`}</Code>191 <p className="mt-2 text-[12px] text-fg-muted">Quoted values allowed; unknown keys stay in the free-text part. Works in <code>/events?q=</code>, <code>/search?q=</code> and the ⌘K palette.</p>192 </Panel>193 <Panel title="Conventions">194 <ul className="list-disc space-y-1 pl-5 text-[12.5px] text-fg-muted">195 <li>All timestamps are UTC ISO-8601. Ids are prefixed (<code>evt_ chg_ snap_ clu_ src_ sen_ ent_</code>).</li>196 <li>Cursor pagination: pass <code>nextCursor</code> back as <code>cursor</code>.</li>197 <li>Scores are 0–100. Signal, importance and confidence are independent; <code>score_reasons</code> explain each signal score.</li>198 <li>Every event traces to change → snapshots → sensor → source; snapshots are immutable. Retention may drop raw bodies (<code>has_raw: false</code>) but never canonical forms or hashes.</li>199 <li>Labels OBSERVED / INFERRED / CONFIRMED / UNCONFIRMED are always exposed; AI text is labelled analysis.</li>200 <li>201 Errors share one shape: <code>{`{"error": "snake_case_code", "detail"?: "…"}`}</code>. Validation failures add <code>details</code> (issues); 404 is <code>not_found</code>, 429 <code>monitor_limit_reached</code> / <code>too_many_*</code>.202 </li>203 <li>204 Rate limit 600 req / min / IP on <code>/api/*</code>, exposed as <Chip className="font-mono">x-ratelimit-limit</Chip> <Chip className="font-mono">x-ratelimit-remaining</Chip> <Chip className="font-mono">x-ratelimit-reset</Chip>; every response has <code>x-request-id</code>.205 </li>206 <li>Planned: API keys, MCP server, e-mail / Slack / Discord channels.</li>207 </ul>208 </Panel>209 </aside>210 </div>211 </>212 );213}214