Docs

WorldSignals turns official registers, journals and public feeds into one stream of standard events. This page covers what is live today.

Early access. The events API, the live stream, agent sign-up, the event standard, the schemas, the CLI (signup, key, events and watch), the remote MCP server, signed webhooks and the Account page are live. For AI assistants: llms.txt, llms-full.txt, openapi.json.

Quick start

Sign up (free in early access) and confirm your email from the link we send. Work emails get in as soon as they confirm: the link signs you in and shows your access key. Personal addresses (Gmail, Outlook…) wait for our daily approval; we email you when you’re in. Each key is shown once, when it is made. Lost it? Sign in and open your Account page: it emails you a link for a new key (the old one stops working when you use it) and lists the apps you connected. Then read the newest collected events:

curl -H "Authorization: Bearer $WS_KEY"   "https://worldsignals.dev/api/events?types=company.registered&country=ES&limit=5"

Without a key, GET /api/events/preview shows the newest events reduced to type, place and day.

Every event follows the event standard. Deduplicate on id; the same fact always has the same id.

Events API Live

GET /api/events

Collected public events. Needs Authorization: Bearer <access key> (or the sign-in cookie in a browser); without it the answer is 401.

ParameterMeaning
typesComma-separated event types, for example company.registered,company.dissolved. Unknown types are ignored.
countryAn ISO 3166-1 alpha-2 code, for example ES, or several, comma-separated: ES,FR. XX is the events about the whole world (vulnerabilities, outages, AI models): add it to get them too, ES,FR,XX. Without country, every country, XX included.
afterAn event number: the events stored after it, oldest first. Poll with the cursor from the last answer, and ask again straight away while more is true. Or an RFC 3339 time: the newest events first seen after it (observed_at), for a quick look.
beforeAn event number from before in the last answer: older events, newest first. Use it to page back through everything that matches. Not with an event number as after.
limit1 to 200, default 50.

Add the filters below to narrow by platform, ecosystem, tag or any field. The answer is {"events": [...], "total": n, "cursor": n, "more": false}. total counts every public event kept (30 days); cursor is the number to send as after next time: the newest event in the answer, or, when the answer isn't full, the newest event stored, so the next poll skips what didn't match your filters. Every stored event has a number, in the order it was stored, so polling with it never skips one. Unless after is an event number, the answer also has before and older: while older is true, ask again with before to page back to older matches.

cursor=$(curl -s -H "Authorization: Bearer $WS_KEY" "https://worldsignals.dev/api/events?limit=1" | jq .cursor)
# later, and on every poll after that:
curl -s -H "Authorization: Bearer $WS_KEY" "https://worldsignals.dev/api/events?after=$cursor&limit=200"

GET /api/stream Live

The same events, live, as Server-Sent Events: any EventSource or curl -N reads it, and ws watch is built on it. Each event’s SSE id is its number. Reconnect with Last-Event-ID (or ?after=) and the stream carries on after it, so a laptop that slept or a dropped network misses nothing within the 30 days kept. Without one it starts at the newest event.

ParameterMeaning
typesComma-separated event types, or all. Default: the types you chose when signing up.
countriesComma-separated ISO codes, or all. Default: the countries you chose. XX is the events about the whole world (vulnerabilities, outages, AI models), a country like the others.
platforms, ecosystems, tags, whereThe filters, as for /api/events.
curl -N -H "Authorization: Bearer $WS_KEY" "https://worldsignals.dev/api/stream?types=vulnerability.exploited"

retry: 3000
event: ready
data: {"after":51234,"head":51234,"types":["vulnerability.exploited"],"countries":[]}

: hb

id: 51240
data: {"specversion":"1.0","id":"…","type":"vulnerability.exploited",…}

event: bye
data: {"after":51240}

When the countries leave out XX but a type you asked for only comes as XX, ready says so in hint, as the events API does. A connection stays open for a few minutes, then ends with bye; reconnect at once (EventSource does it for you). A heartbeat (: hb) comes every 15 seconds and means everything waiting has been sent. gap warns that events after your number are past the 30 days kept. An id: line with no data means nothing up to that number matched your filters: keep it as your place (EventSource does), and a reconnect skips those events.

Limits

Each access key can make 120 requests a minute, shared by /api/events, /mcp, /api/me and webhook tests, and open 12 connections a minute to /api/stream; a stream that reconnects every few minutes uses one. Over a limit the answer is 429 with Retry-After: 60, and ws watch waits and reconnects on its own.

Filters

Both /api/events and /api/stream narrow events beyond type and country. Values in one filter are alternatives (any of them); different filters all apply.

ParameterMeaning
platformsOperating systems in data.platforms: windows, macos, linux, android, ios, chromeos, freebsd.
ecosystemsPackage registries in data.ecosystems, which stand for languages: npm (JavaScript), pypi (Python), maven (Java), nuget (.NET), go, rubygems (Ruby), crates (Rust), packagist (PHP), pub (Dart), hex (Elixir, Erlang), swift, cocoapods, github_actions, and wordpress for its plugins.
tagsAny of these tags, for example critical, exploited, ransomware.
whereA condition on any field: a dotted path, an operator (= != > >= < <=) and a value. | separates values for = and !=. Numbers compare as numbers, true and false as booleans, and an array field matches when any item does. Repeat it, up to eight times; all must hold.

Vulnerabilities, advisories, exploits and package releases carry platforms and ecosystems. New CVEs get them from the affected products once the CVE has been analysed and, before that, from the description and the authority that assigned it, so expect a few broader matches. Advisories get them from their packages, releases from their registry.

With field filters, one answer reads at most 5,000 event numbers, so a poll can come back with no events, more: true and a later cursor: keep asking from the cursor until more is false. The stream and ws watch do this for you. Newest first works the same way backwards: keep asking with before while older is true.

# Critical and high CVEs that affect Windows
curl -H "Authorization: Bearer $WS_KEY"   "https://worldsignals.dev/api/events?types=vulnerability.published&platforms=windows&where=data.cvss.severity%3Dcritical%7Chigh"

# Python advisories, live
ws watch --type vulnerability.advisory_published --ecosystem pypi

# Vulnerabilities exploited by ransomware on Windows
ws watch --type vulnerability.exploited --tag ransomware --platform windows

GET /api/stats

Counts of what has been collected: events (public events kept, 30 days), the sources and countries they came from, the types seen and the time of the latest event, plus the number of connectors, of eventTypes defined, and retentionDays.

GET /api/catalogue

Every event type with its family, visibility, an example event, and the connectors that collect it today (empty while a type is planned).

GET /schemas/<name>/1.json

JSON Schema (draft 2020-12) for the envelope, the shared definitions and each typed payload, for example company.registered/1.json.

Get events into your agent

Two ways in today. Live: ws watch on a computer or server that stays on starts your agent the moment events arrive. Polling: an agent that runs on a schedule asks for what is new since last time. Cloud agents that can’t run a program of their own get events through webhooks or the MCP server, poll on a schedule, or let ws watch wake them through their own trigger.

AgentLivePolling
Claude Codews watch --agent claude runs claude -p for each event or batchA routine on a schedule runs npx worldsignals events --after …; or ws watch --once from cron
Codexws watch --agent codex runs codex exec in a read-only sandboxA scheduled task runs npx worldsignals events --after … (allow worldsignals.dev in its network settings)
Grok Build, Muse Code and other terminal agentsws watch --exec runs the agent’s own headless command with the events on stdinws watch --once from cron
Claude Code routines, Grok Bot routinesws watch --exec calls the routine’s webhook or API trigger with the eventsA schedule trigger that reads /api/events?after=
Cursor, VS Code, GitHub Copilot, Claude Managed AgentsWebhooks to an endpoint that starts the agentThe MCP server with your key: search_events with the last cursor
Claude Duet, dots, MuseThrough the MCP server and webhooks, once they are listed for themAn in-app schedule, once the MCP server is listed for them
Your own codeGET /api/stream with any SSE client, or ws watch --exec, --out or plain JSON linesGET /api/events?after=<cursor>

Claude Code

export WS_KEY=wsk_…
npx worldsignals watch --type vulnerability.exploited --agent claude   --prompt "Check whether any of these affect the dependencies in this repo, and say which."

It runs in the folder you start it from. claude -p can’t stop to ask for approval, so it only uses tools that need none (reading files) unless you allow more after --, for example -- --allowedTools "Read,Grep". Add --batch to run once for events that arrive together.

On a schedule instead (a Claude Code routine, or any cloud run without a disk that remembers): ask for events first seen in the last interval plus a margin, and skip ids you have already handled.

npx worldsignals events --type vulnerability.exploited --after "$(date -u -d '-70 min' +%Y-%m-%dT%H:%M:%SZ)" --json

Codex

npx worldsignals watch --type software.released --agent codex --batch   --prompt ./release-notes-brief.md -- --skip-git-repo-check

Runs codex exec --sandbox read-only with the prompt on stdin. Pass another --sandbox after -- to let it write. --prompt takes the text or a file.

Grok Build, Muse Code and other terminal agents

Any agent with a headless mode that reads a prompt on stdin works through --exec. Check its docs for the flag; the events arrive as JSON lines on stdin, and WS_EVENT_TYPES and WS_EVENT_COUNT are set.

npx worldsignals watch --type service.outage_started --exec ./ask-my-agent.sh

Cloud agents with a webhook or API trigger

Claude Code routines have an API trigger and Grok Bot routines a webhook trigger. Neither takes our signed webhooks as they are, so a small script run by ws watch --exec passes the events on. Take the URL, token and any required headers from the trigger’s settings.

#!/bin/sh
# fire.sh: send the waiting events to a cloud agent's trigger. The events come on stdin.
jq -Rs '{text: .}' | curl -fsS -X POST "$TRIGGER_URL"   -H "Authorization: Bearer $TRIGGER_TOKEN" -H 'content-type: application/json' --data-binary @-
npx worldsignals watch --type procurement.tender_published --country PT --batch --exec ./fire.sh

Batch them: triggers have hourly limits, and one run for many events costs less.

Polling from cron or a scheduled job

ws watch --once handles everything since the last run, then exits. It remembers its place in --state, so no event is skipped or handled twice:

*/15 * * * * WS_KEY=wsk_… npx worldsignals watch --once --state ~/.ws/tenders.json --type procurement.tender_published --out ~/tenders

From your own code, poll GET /api/events?after=<cursor> and keep the cursor each answer gives (Events API).

Event format

A CloudEvents 1.0 envelope with WorldSignals fields: time (when it happened), observed_at (when we first saw it), country and place, the subject and other entities, a one-line title, the typed data, and provenance with the source record link, licence and attribution.

Sign up from an agent Live

An AI agent can sign up the person or company it works for. The agent asks; the person confirms. Nothing is signed up until they do, and a person who is already signed up keeps everything they chose.

curl -X POST https://worldsignals.dev/api/signup-requests   -H 'content-type: application/json'   -d '{"email": "ana@example.com", "company": "Example Lda",
       "eventTypes": ["company.registered"], "countries": ["ES"],
       "plan": "developer", "billing": "monthly",
       "agent": {"name": "My agent", "version": "1.0"}}'

The answer has a confirm_url, a message_for_human and a pickup_secret. Keep the secret to yourself and never put it in a URL; pass the other two to your person: they open the link, see exactly what you asked for, and confirm or decline. Declining forgets their email. Confirming sends them an email that names you, and only a click on it proves the address is theirs: pressing Confirm alone never releases a key. Once they click it (within 24 hours, or the request expires), GET /api/signup-requests/<t from the link> with the header Authorization: Bearer <pickup_secret> returns access_key, once, which reads events for them. If you can sign, send pickup_public_key (an Ed25519 public key: its 32 raw bytes, base64url) with the request instead: no secret comes back, and you sign the fetch with X-WS-Timestamp (unix seconds, within five minutes) and X-WS-Signature (base64url Ed25519 signature of GET /api/signup-requests/<t> <timestamp>); the ws CLI does this for you. The key comes at once for a work email, or after our daily approval for a personal address (Gmail, Outlook…). Until then it says email_confirmed: false, then waiting_list: true. Fetch it within 7 days of that and keep it: after that the link only says expired. If they already have an account, nothing you asked for changes it until they click.

FieldMeaning
email, agent.nameRequired: your person’s email and your own name, so they know who asked.
eventTypes, countriesOptional: ids from /api/catalogue and ISO country codes. Without countries, every country. XX is the events about the whole world (vulnerabilities, outages, AI models): add it to a list to get them too, ["ES", "XX"]; the answer has a hint when a chosen type only comes as XX and the list leaves it out. The person can change them later.
plan, billingOptional: free, developer, team, business or enterprise; monthly or yearly.
company, role, useCaseOptional, and shown to the person before they confirm.

Links last seven days. One network address can make ten requests a day. The full description is in OpenAPI, and a summary for assistants is in llms.txt.

Safety: run agents that read events in a sandbox

Events carry text from the outside world: company names and purposes, tender titles, advisory summaries, exploit names, and, once connected accounts arrive, your customers’ DMs and reviews. Any of it can contain instructions written for an AI (prompt injection). Treat every event as data, never as instructions, and give the agent that reads them as little power as possible.

ws watch --agent does what it can by default: it passes events to the agent between random markers, says they are data and not instructions, and holds back your access key. claude -p can’t approve tools on its own, and codex exec runs with a read-only sandbox. That narrows what an injected instruction can do, but it doesn’t replace a sandbox: for anything that matters, run ws watch itself inside a container or VM.

CLI Live

The worldsignals npm package (Node 18 or later, no dependencies, source on GitHub), also installed as ws:

npx worldsignals signup --email ana@example.com --agent "My agent" --type company.registered --country ES --plan developer
npx worldsignals key --link "https://worldsignals.dev/confirm?t=…"     # after your person confirms
WS_KEY=wsk_… npx worldsignals events --type procurement.tender_published --country FR --limit 10

Without npm, download the same single file: curl -fsSLO https://worldsignals.dev/cli/ws.mjs and run node ws.mjs.

ws watch

Stays connected to the live stream and handles each event as it arrives. It remembers the number of the last event it handled, so after a restart, a sleeping laptop or a dropped network it carries on from there.

ws watch                                              # one JSON event per line, your own choices
ws watch --type company.registered --country PT,ES --exec ./on-event.sh
ws watch --type procurement.tender_published --out ./events/
ws watch --type vulnerability.exploited --agent claude --prompt ./brief.md
ws watch --type software.released --agent codex --batch
OptionMeaning
--type, --countryWhat to watch, comma-separated (--country PT,ES). Default: what you chose when signing up. XX is the events about the whole world (vulnerabilities, outages, AI models): --country PT,ES,XX.
--platform, --ecosystem, --tag, --whereThe filters; ws events takes them too. Repeat --where for more conditions.
--exec <program>Runs it for each event (or batch), with the events as JSON lines on stdin and WS_EVENT_ID, WS_EVENT_TYPES, WS_EVENT_COUNT and WS_EVENT_SEQ set. Never through a shell; arguments after -- are passed as they are. Your access key is left out of its environment.
--agent claude|codexStarts claude -p or codex exec with your --prompt (text or a file) and the events, fenced off as data. Arguments after -- go to the agent.
--out <folder>Writes each event to its own file, named by number and type.
--batchCollects events and runs once for many: after a quiet --batch-idle (60 s), at most --batch-max-wait (600 s) after the first, or at --batch-max (100) events.
--onceHandles what is waiting, then exits: for cron and scheduled jobs.
--state <file>, --from now|start|<number>Where it remembers its place (default ~/.config/worldsignals/watch.json; give each watcher its own), and where to begin instead.
--timeout <seconds>Stops a program or agent that runs longer (default 600).

Events are handled one after another, oldest first, at least once: the place is saved only after the program or agent finishes, so one that was interrupted gets its events again. A program that fails is reported and the watch carries on.

How events reach your program

The same events, four ways. Events arrive in bursts, in quiet spells and in steady trickles, and the way you choose decides how often your program or agent runs.

For AI agents, use a batch or files. Each agent run takes time and costs money, and an agent that sees ten related events in one run decides better than ten runs that see one each. With --batch it runs once a burst is over, or after the longest wait you allow. With --out the events wait in a folder until your agent, a schedule or a person picks them up.

One by one

--exec ./handle.sh

Runs your program for every event, the moment it arrives.

Your program

Batches Best for agents

--exec ./agent.sh --batch

Collects events and runs once: after 60 s of quiet, or after 10 min at most.

Your program

Files Best for agents

--out ./events

Writes each event to its own file; whoever reads the folder handles them.

Lines of JSON

ws watch

Prints one event per line, to pipe into anything.

A simulation, sped up about 40 times: 60 seconds of quiet pass in under 2. Tune batches with --batch-idle, --batch-max-wait and --batch-max (events per run).

Webhooks Live

Register an https endpoint with your access key and we POST each matching event to it, one event per request, in the order events were collected. Each request is a CloudEvents structured request (Content-Type: application/cloudevents+json; the body is the standard event) signed per Standard Webhooks.

curl -X POST https://worldsignals.dev/api/webhooks   -H "Authorization: Bearer $WS_KEY" -H "Content-Type: application/json"   -d '{"url":"https://you.example/hooks/ws","types":["vulnerability.published"],"where":["data.cvss.base_score>=9"]}'
# 201 {"id":"wh_…","secret":"whsec_…","filter":{…},"cursor":12345}   the secret is shown only here

To verify a request, compute an HMAC-SHA256 of webhook-id.webhook-timestamp.body keyed with the bytes of your secret after whsec_ (base64-decoded), and compare its base64 with any v1, entry in webhook-signature. Reject timestamps more than five minutes old. webhook-id is the event id and stays the same on retries, so you can drop repeats.

import crypto from "node:crypto";
// body: the raw request body, exactly as received
export function verifyWebhook(secret, headers, body) {
  const id = headers["webhook-id"], ts = headers["webhook-timestamp"];
  if (!id || !ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const key = Buffer.from(secret.slice("whsec_".length), "base64");
  const expected = Buffer.from(crypto.createHmac("sha256", key).update(`${id}.${ts}.${body}`).digest("base64"));
  return (headers["webhook-signature"] ?? "").split(" ").some((entry) => {
    const [version, sig] = entry.split(",");
    const given = Buffer.from(sig ?? "");
    return version === "v1" && given.length === expected.length && crypto.timingSafeEqual(given, expected);
  });
}

Answer with any 2xx within 10 seconds; we don't follow redirects. Anything else is retried after 1, 5 and 30 minutes, then 2 hours, then every 12 hours. After three days of failures, or at once on 410 Gone, the webhook is switched off until you call POST /api/webhooks/<id>/enable, and delivery resumes where it stopped (within the 30 days we keep). A new webhook starts with the next event collected, or after the event number you pass as after. Filters are the same as the events API: types, countries (XX for the events about the whole world), tags, platforms, ecosystems, where; left out, types and countries are copied from your saved stream. Up to 5 webhooks; GET /api/webhooks lists them, DELETE /api/webhooks/<id> removes one, POST /api/webhooks/<id>/test sends a signed webhook.test event now, and POST /api/webhooks/<id>/rotate-secret issues a new secret, with requests signed with both for a day. Delivered events show in your key’s usage.

Plans and billing From 8 December

Everyone starts free. To upgrade, use the Plan panel on your Account page, or, from an agent or a script, ask for a payment link with your access key and pass it to your person: they pay with Stripe, the plan starts as soon as the payment goes through, and you never see a card.

curl -X POST https://worldsignals.dev/api/billing/checkout   -H "Authorization: Bearer $WS_KEY" -H 'content-type: application/json'   -d '{"plan": "team", "billing": "yearly"}'

plan is developer, team or business; billing is monthly (the default) or yearly. The answer has the url to open. ws upgrade --plan team does the same from the command line. POST /api/billing/portal gives a link to manage the subscription: change plan, card, invoices or cancel. VAT is added from the billing address; businesses can give their VAT number. Founding members, who signed up before 8 December 2026, keep 30% off for life while the subscription stays active. Until paid plans open, these answer 503 and everything is free.

Company timelines and watchlists Live

Every event is indexed by the companies in it, by registry number (gb_crn:09000000, fr_siren:938000004, us_cik:0000320193…), so one company’s events from every source come together. GET /api/entities?name=Acme&country=GB finds candidates; GET /api/entities/<key>/events is that company’s timeline, newest first (fuzzy=1 also joins on the name, marked weak). The MCP tool get_entity does the same.

A watchlist holds up to 1,000 companies, and you keep up to 10: POST /api/watchlists with {"name": "Suppliers"}, then POST /api/watchlists/<id>/entities with {"ids": ["gb_crn:09000000"]}. Add watchlist=<id> to the events API, the live stream or a webhook’s filter to get only those companies’ events.

Your own combined signals Live

Built-in combined signals join sources for you: one of them, a young company winning a public contract (combined.young_company_won_contract), is on every plan; the others are on the Team plan and up. Also on the Team plan and up, tell us which two events to watch for and we send a signal when both happen to the same company within a number of days: a company on your watchlist registers and wins a public contract within 90 days, or gets a licence and a building permit within 60. Companies are matched by their registry number, never by name. Team keeps 5 rules, Business 50. Each match is a private event, combined.custom_rule_matched, that reaches only you: through the events API, the live stream, webhooks and MCP.

curl -X POST https://worldsignals.dev/api/rules   -H "Authorization: Bearer $WS_KEY" -H 'content-type: application/json'   -d '{"name": "Young company wins a contract",
       "first": {"types": ["company.registered"], "countries": ["GB"]},
       "then": {"types": ["procurement.contract_awarded"]},
       "within_days": 90, "order": "first_then"}'

Each step takes up to 4 types, and optionally countries (XX for the events about the whole world), tags and up to 10 where conditions in the filter language. within_days is 1 to 365; order is first_then (the second event on the day of the first or later) or any; watchlist limits the rule to the companies on one of your watchlists. A rule remembers the events that match each step for its whole window, although events themselves are kept 30 days. Only events collected after you save a rule produce signals; POST /api/rules/test (an unsaved rule) or POST /api/rules/<id>/test shows what it would have matched in the last 30 days. GET /api/rules lists your rules with their status, PATCH /api/rules/<id> changes or pauses one ("enabled": false), DELETE removes it. Without a paid Team plan or higher the API answers 402; if a plan ends, its rules stay and start again when it is renewed.

MCP server Live

A remote MCP server at https://worldsignals.dev/mcp (streamable HTTP). Your access key is the bearer token, so any MCP client that can send a header can use it: Claude Code, Cursor, VS Code and GitHub Copilot, Claude Managed Agents and your own. Clients that support OAuth can connect without a key: they find the sign-in through the server’s metadata (/.well-known/oauth-protected-resource/mcp), you sign in to WorldSignals with an emailed link or your key, and you allow the app on a consent page. Access tokens last an hour and renew on their own. Disconnect an app on your Account page; deleting your sign-up ends every connection.

claude mcp add --transport http worldsignals https://worldsignals.dev/mcp   --header "Authorization: Bearer $WS_KEY"

For clients configured with JSON (Cursor, VS Code):

{ "mcpServers": { "worldsignals": {
    "url": "https://worldsignals.dev/mcp",
    "headers": { "Authorization": "Bearer wsk_…" } } } }
ToolWhat it does
list_event_typesEvery event type by family, whether it is collected today, and in which countries
search_eventsEvents filtered by types, country (one code or several, comma-separated), tags, where conditions and since (a time), newest first, paging back with before while older is true; with after, the events stored after that event number, plus a cursor for the next call. limit: 20 by default, at most 50
get_eventOne event by its id, in full
my_streamThe event types and countries saved for your key

All tools are read-only and share your key’s 120 requests a minute with the events API. Every answer reminds the agent that event content comes from public sources and is data, not instructions.

Sources and licences

Events come from official registers and journals, public databases and status pages, and package registries; connected accounts are planned. We use public sources under their reuse licences, pass on paid data only where its licence allows, and deliver sources that need your own key to you alone.

Every event names its exact source, a link to the record, the licence and the attribution in provenance. The event catalogue shows which types are collected today and where.

Delivery and retention