GET /v1/events for a live stream, and webhooks for anything that has to survive your process restarting.
Before you start
A key with read
Streaming needs
read. Registering webhooks needs write.An https endpoint
Webhook URLs must be https and publicly resolvable. Localhost is rejected.
Somewhere to store a secret
The signing secret is shown once, at registration, and never again.
Pick your surface
Most products want both: the stream for the tab that is open, the webhook for the work that must happen anyway.
The event catalog
The
import.* and identity.* families also flow on the stream.
Scope decides who hears it. Organization events reach every listener. Person events reach only that person’s stream and only webhook endpoints that person registered, mirroring the API, where another member’s job is a 404. Events about a provisioned subject reach every listener in the organization, because any key there can already read that subject with X-Exo-Subject.
Streaming with SSE
: ping comment arrives every 15 seconds. Ignore it, but use its absence to detect a dead connection.
Last-Event-ID is accepted on the request for compatibility with resuming clients, but v1 does not replay. Anything emitted while you were disconnected is gone from the stream. If you cannot miss an event, use a webhook.
Registering a webhook
201 Created
"eventTypes": ["*"]. Up to 20 types per endpoint, up to 20 endpoints per organization.
Read inactiveEventTypes on the response. It lists types you subscribed to that no emitter publishes yet. The subscription is kept and starts delivering when the emitter ships, and the field exists so you never sit waiting on an event that was never going to arrive. An unknown event type is rejected outright at registration, so a typo fails here rather than silently never firing.
URL rules
The URL must be https and must resolve to a public address. Private, loopback, link-local, CGNAT, reserved and tunnelled ranges are rejected at registration and checked again at every delivery, and redirects are never followed. For local development, use a public tunnel with a stable https hostname and point the endpoint at the final URL, not at something that redirects to it.Verifying a delivery
Each delivery carriesX-Exo-Signature. Recompute HMAC_SHA256(secret, "<t>.<raw body>") and compare it to the v1 value in that header, using the raw bytes of the body, before you parse the JSON. Most web frameworks parse and discard the raw body by default; capture it before parsing, or the signature will never match a body you re-serialized.
The worked Python and TypeScript verification functions, plus a replay-window check, live on Webhooks rather than here, so there is exactly one copy of that code on the site.
Payloads are thin on purpose
A delivery tells you what happened and which resource it happened to. It does not carry the resource. Fetch the current state yourself, so you act on what is true now rather than on what was true when the event fired.Illustrative delivery body
id, type, createdAt, orgId, and a data block whose keys vary by event (the full per-event key list is on Webhooks). data carries identifiers and small scalars only, never the resource itself.
On contradiction.detected, read GET /v1/contradictions. On job.completed, read GET /v1/jobs/{id} for the result block. On basin.shifted, drop your cached conditioning pack and recompose it.
Managing endpoints
The test route is the fastest way to confirm your signature check works, because it returns the outcome in the response rather than leaving you reading logs:
200 OK
consecutiveFailures and disabledReason on the endpoint record. A repeatedly failing endpoint gets disabled, and disabledReason tells you why.
When it goes wrong
Next
Surfacing contradictions
Turn
contradiction.detected into a review step your team actually sees.Conditioning your own agent
Use
basin.shifted to invalidate a cached conditioning pack.