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.