REST API

Base URL: https://api.openwake.ai (development: http://localhost:8787). All responses JSON. Public routes need no auth; org routes need Authorization: Bearer <api key> (or, from the web app only, the person's sealed session in x-openwake-session plus x-openwake-org).

Health

GET/healthliveness for load balancers
GET/health/deepdatabase, migrations, keys, ledger head and anchor age, pipeline freshness, integrations; 503 with problems[] when something blocks production.

Public: the registry

MethodPathPurpose
GET/v1/statsregistry counters
GET/v1/vendorsall vendors with latest training stance
GET/v1/vendors/:slugfull cited profile: facts, chain, documents, changes, document changes
GET/v1/vendors/lookup?domain=notion.sothe vendor behind a domain (website and document hosts first, then the name): the full profile plus verdict (training stance in one line with its quote), profile_url, watch_url. What the browser badge asks for the tab it is on. 404 when nothing is on record.
GET/v1/changes?limit=20recent fact changes
GET/v1/concentration?vendors=a,bwhich hyperscalers / model providers a vendor set terminates in
GET/v1/agents/:agent_ida registered agent and its operator: your own organization's with a key, or one that belongs to no organization. Never another organization's; an agent id is unique within an organization, not across them
POST/v1/check{ counterparty, data_classes[], caller_agent_id? } → advisory decision + signed receipt. Attributed to your org when a key is sent.

Public: proofs

GET/v1/keyspublic key, key id, hash recipe
GET/v1/ledger/headchain head
POST/v1/receipts/verifybody: a receipt from /v1/check (the chain is consulted only when the receipt's own signature verifies; otherwise ledger is null) → { ok, offline: { statement_hash, entry_hash, signature }, ledger: { present, matches } }. The offline part needs only the public key;
GET/v1/ledger/:kind/:refledger entry for a record. For anyone: the public registry's kinds (fact_set, snapshot, change, document_change). With your organization's key: your own check, agent_event, footprint and finding records. Anything else is 404. A receipt does not need this route: it verifies offline, and POST /v1/receipts/verify answers for a genuinely signed one

Self-serve

POST/v1/importBring your own vendor list. Body: raw text/csv or text/plain (names, domains, URLs, or a CSV export from Okta / Google Workspace / Entra / a SaaS-management tool), or JSON { text, watch?, email?, slack_webhook_url? }. Returns per-vendor matches with training / AI-feature / subprocessor summary, the unmatched rows, and a coverage percentage. With watch + email it also starts a watchlist (creating an org + API key, or adding to yours when a bearer key is sent). Unmatched vendors are counted for coverage: never who asked. The web form at /import calls the same code.
POST/v1/signup403 when sign-in is configured (accounts start at /login). Otherwise { email, vendors[], slack_webhook_url? } → org + API key (shown once) + watchlist. Rate-limited. The web form at /watch calls this.
POST/v1/vendors/:slug/corrections{ field, claim, source_url?, contact_email? } → a public correction request, ledger-entered. field is a fact key, subprocessors, documents or other. Rate-limited.
GET/v1/vendors/:slug/correctionsthe public correction log for a profile (never the contact)
POST/v1/vendors/:slug/claim{ email } at the vendor's domain → emails a 48-hour verification link (dev: returned as dev_link)
GET/v1/claim/:tokenredeems the link; marks the vendor profile verified

Org-scoped

POST/v1/watchlists{ owner_email, vendors[], slack_webhook_url? }: secrets sealed at rest
POST/v1/agents (also parent_agent_id, kind, policy)register an agent: { agent_id, name, operator_vendor?, issuer?, scopes? }
GET/v1/agentsyour registered agents
POST/v1/footprinta Scout inventory → vendors resolved against the registry, unmatched hints queued, stored, ledger-entered.
GET/v1/footprintlatest report per device; vendors seen across devices with stance, how they were seen (tool, MCP, key), and whether they are watched
GET/v1/org/landscapewatched ∪ seen vendors and concentration over what is underneath them
GET/v1/checks?limit=100your check log
GET/v1/dataflow?days=90data classes → counterparties summary (the Article 30 view)
GET/v1/api-keyslist the org's keys: name, prefix, created, last used, revoked
POST/v1/api-keysmint another key (shown once). A signed-in owner only: an API key or a device token is refused (owner)
POST/v1/api-keys/:id/revokerevoke a key (owner)
GET/v1/mesigned-in person, their orgs and roles, and for each org free_limit_exceeded_at when it went past Free (null where billing is not open) (session only)
POST/v1/me/orgs{ name } → a new org on Free with the caller as owner (session only)
GET/v1/me/invitationsopen, unexpired invitations to the signed-in person that their account may accept: any when its address was proven by an emailed code, Google or a verified Microsoft domain; only its own organization's for an account a customer's SSO created (session only)
POST/v1/me/invitations/:id/acceptjoin that organization with the invitation's role; 404 when it is not open to this account (session only)
POST/v1/me/invitations/:id/declineremove the invitation and free its seat; recorded in that organization's audit log (session only)
GET/v1/org/membersmembers, open invites, seats used / limit. A signed-in person only: an API key or a device token is refused
POST/v1/org/members{ email, role? } → an invitation that waits for them to accept it, emailed to them and good for 14 days, whether or not they have an account (201, outcome: invited, emailed, expires_at); 200 outcome: already_member and no change for someone already in the organization; 402 when every seat is taken, though re-sending an open invite takes no new seat and renews it (owner)
PATCH/v1/org/members/:userId{ role } change a member's role: owner, admin, member (shown as Analyst) or viewer (shown as Read-only); making an owner or changing an owner's role needs an owner (403 otherwise); 409 when that would leave no owner (owner or admin)
PATCH/v1/org/members/:userId/grants{ billing_manager?, evidence_reader? } give or take away a grant; each change is in the audit log (owner)
DELETE/v1/org/members/:userIdremove a member; the last owner stays, and only an owner removes an owner (owner or admin)
DELETE/v1/org/invites/:idwithdraw an invite (owner or admin)
Every invite, join, role change, removal, withdrawn and declined invite is written to GET /v1/org/audit and sealed by the ledger
GET/v1/org/ssothe org's own OpenID Connect issuer (issuer, client id, email domain, verified_at, and tested_at: when an owner signed in through it, after which /login routes the domain; never the secret), the DNS record that proves the domain (domain_record), and the redirect URI to register (owner)
PUT/v1/org/sso{ issuer, client_id, client_secret?, email_domain } set it; Business only; the secret is required the first time and kept when omitted; a new email domain has to be proven again, and after any edit /login waits for an owner's sign-in before routing the domain again; 409 when another org already proved that domain (owner)
POST/v1/org/sso/verifylook up domain_record (a TXT record at _openwake.<domain>) and mark the domain proven: 200, 422 while the record is not published, 409 when another org proved the domain first. Until the domain is proven the provider signs nobody in, and it only ever signs in addresses at that domain (owner)
DELETE/v1/org/ssoremove it (owner)
PATCH/v1/agents/:id{ name?, parent_agent_id?, kind?, policy? } rename, re-parent (submodule), or set the allowed context of an agent this org registered. policy is { data_classes?: [...], counterparties?: [...] }; * allows all, acme/* a subtree; null clears it
POST/v1/agents/:id/events{ kind, counterparty?, data_classes?, outcome?, check_id?, detail?, tool?, action?, acting_for?, delegated_by?, parent_event_id?, task_id?, purpose?, authority? } → the event scored against the agent's policy (within_policy, policy_reasons) and its ledger entry. kind is delegation, tool_call, data_out, data_in, decision or custom; detail is one sealed line, never a payload. 600/min per org. Scout for Agents posts one of these per MCP tool call
GET/v1/agents/:id/eventsthat agent's events, newest first
GET/v1/org/activity?days=90&limit=100 every agent's events for the org
GET/v1/org/activity/flows?days=30&node=column:value the activity view: every event as a path source → actor → action → data class → destination → outcome, aggregated into nodes and weighted edges with the flagged share (blocked or outside policy). With node, only the events through that node, returned as events. What the Agents page's Activity tab draws
GET/v1/org/findings?status=open (default), acknowledged or all: behavior worth a look, from the org's events and nothing else: first contact, a sensitive class to a vendor that trains, the exfil shape (many reads then a send in one session), a burst against the agent's own rate, a destructive action on a newly registered server. Runs the rules first when they have not run in the last minute. See docs/recorder.md, Findings
POST/v1/org/findings/:id/ackacknowledge a finding (members; viewers cannot). The record stays; the finding leaves the open list
GET/v1/org/activity/streamserver-sent events: every agent event (activity) and every Scout report (footprint) as it lands, ?since=<event id> to resume; ping every 16 s, bye after 30 min (reconnect). What /dashboard/live shows
GET/v1/org/overview?days=30 the org's dashboard: watched vendors, changes by materiality, objection windows closing soonest, alerts sent, agent activity
GET/v1/org/changes?days=90&limit=200&handled=0 fact and document changes on the watched vendors, newest first, with the notice window, the handled state and a drafted objection for fact changes
POST/v1/org/changes/:kind/:id/handled{ handled?: true, note? } mark a change handled (or reopen it) for this org
GET/v1/org/vendorsthe vendors the org watches with the latest record's headline facts, notice period, and change counts for 30 days
DELETE/v1/org/vendors/:slugstop watching a vendor across the org's watchlists (owner)
GET/v1/org/graph?days=90 the org's agents as a tree, the counterparties they reached (checks + events, data classes, decisions, out-of-policy counts), and what sits downstream of those vendors

Billing

GET/v1/billingyour plan, its limits, current usage, and meter: the people and systems billed on (last 30 days), what Free covers, what Team and Business cost for them, the subscription's quantities, and whether billing is enabled. The names behind the people count go to a signed-in person only
POST/v1/billing/checkout`{ plan: "team" \"business", email? } → { url }` Stripe Checkout with a person line and a system line at what is in use. 409 for an organization Openwake does not bill; 503 when Stripe is not configured. A signed-in owner only; an API key or a device token is refused (owner)
POST/v1/billing/usage/syncbring the subscription to the people and systems in use: more is prorated now, less applies from the next invoice (owner)
POST/v1/billing/portal{ url } Stripe Customer Portal (change or cancel). A signed-in owner only (owner)
POST/v1/billing/webhookStripe → Openwake. Signature verified against STRIPE_WEBHOOK_SECRET; each event is applied before it is recorded, and a plan only when its event is at least as new as the last one applied

Plan gates return 402 with an upgrade URL: single sign-on (Business), and admin seats beyond the plan (Free: 3, Team: 25). Every plan carries exports, Slack, the data-flow report and an unlimited watchlist; Free is limited to 5 endpoints and 90 days of history. Checks, the registry, evidence packs and JSON/HTML reports are never gated. See docs/pricing.md.