Skip to main content
Exo keeps working after your request returns. Jobs finish, the daemon proposes connections, contradictions surface, a subject’s mode of thinking shifts. Two surfaces tell you: 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

A : ping comment arrives every 15 seconds. Ignore it, but use its absence to detect a dead connection.
An organization may hold 5 concurrent streams. The sixth gets 429 too_many_streams with a Retry-After header. Open one stream per browser tab and you will hit this with six tabs, so multiplex through your own backend rather than opening a stream per client. See too_many_streams.
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
The signing secret is shown once, in this response, and there is no route that returns it again. Store it before you close the connection. If you lose it, register a new endpoint. An idempotent replay of the same registration returns the original record with secret replaced by a marker, because the plaintext is never persisted, not even for replay.
Subscribe to everything with "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 carries X-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
Every delivery uses this same envelope regardless of event type: 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
Watch 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.