Event stream (SSE)

The API exposes a server-sent event (SSE) stream that delivers validation events in real time. This is the same stream that evctl listen subscribes to internally.

On this page

Connecting

GET /api/v1/events/stream
X-Api-Key: ev_your_key
Accept: text/event-stream

The connection stays open and the server pushes events as they occur.

A browser EventSource cannot set request headers, so pass the key in the query string instead:

GET /api/v1/events/stream?api_key=ev_your_key

The stream requires the read scope, like every other GET. See Authentication.

What the stream carries

The same resources the list endpoints return. One key means one view of your account's API resources, whichever endpoint asks — so the stream is scoped exactly as GET /api/v1/verify and GET /api/v1/verify/batch are:

  • Your account's work, not only the work of the user whose key it is. On a team account, every member's API submissions reach a key on that account.
  • API-submitted work only. Validations you start in the dashboard are the dashboard's; they never appear on the API stream.
  • Your key's mode. An ev_test_ key streams sandbox events, a live key streams real ones, and never the other way round.

If a verify.completed frame is missing, the same rule tells you why: check whether the verification appears in GET /api/v1/verify for that key. If it does not, the stream will not carry it either.

The stream is not rate-limited

GET /api/v1/events/stream is deliberately outside the per-account request budget: a reconnect loop would otherwise burn a per-minute allowance and cost you your event feed exactly when your integration is misbehaving. It therefore also returns no X-RateLimit-* headers. See Rate limits.

Resuming after a disconnect

Reconnect with the Last-Event-ID header set to the id of the last event you received, and the server replays what you missed before resuming the live stream. This is the standard SSE reconnect protocol, so most SSE clients do it for you.

GET /api/v1/events/stream
Last-Event-ID: 638852745311234567
X-Api-Key: ev_your_key
Accept: text/event-stream

The ?since= query parameter does the same thing from an explicit point in time. It accepts either an ISO-8601 timestamp or the same tick count an event id carries:

GET /api/v1/events/stream?since=2025-06-10T14:00:00Z
X-Api-Key: ev_your_key
Accept: text/event-stream

Last-Event-ID wins when both are supplied. With neither, the stream starts from now.

Replay is capped and covers completed validations only, so treat it as a catch-up convenience rather than an audit log. It applies the same scoping as the live stream — the replay and the live feed are one population, not two. The GET endpoints remain authoritative — including the list endpoints, which recover work whose id you no longer hold.

Heartbeats

The server sends a periodic : (comment) heartbeat to keep the connection alive through proxies and load balancers. SSE clients ignore comment lines automatically.

Event format

Each frame carries an id, an event name, and a data payload. The id is a UTC tick count — the cursor you send back as Last-Event-ID. The data value is the same JSON envelope used by webhooks:

id: 638852745311234567
event: verify.completed
data: {"id":"evt_3a4b5c6d...","event":"verify.completed","created_at":"2025-06-10T14:02:11.432Z","data":{...}}

Note the difference between the two ids: the SSE frame id is a stream cursor (a tick count), while the envelope's own id is the stable evt_… event id you deduplicate on. They are not interchangeable.

See Webhooks for the full envelope and event-type reference.

When to use SSE vs webhooks

SSE Webhooks
Requires public URL No Yes
Works in browser Yes No
At-least-once delivery No (replay is capped and completed-only) Yes — up to 22 attempts over ~3 days
Signed No (the connection is authenticated instead) Yes (X-Signature)
Good for Dashboards, local dev Production integrations

For local development without a public URL, evctl listen subscribes to the SSE stream and forwards events to your localhost endpoint — already signed — so your real verification code runs against real payloads. See CLI.