Event streams

An event stream delivers the same public events as GET /events over Server-Sent Events (SSE), in the same order and with the same JSON, so you don't need to poll. Each event stream belongs to the credential that created it and has a durable cursor over the public event log, an expiration time, and a limit of one connection at a time. The cursor points into the public event log; events are not copied per stream.

For the full schema of each operation, see the EventStreams reference.

Permissions and limits

PermissionOperations
event_streams.readGET /event-streams, GET /event-streams/{event_stream_id}, GET /event-streams/{event_stream_id}/events
event_streams.managePOST /event-streams, DELETE /event-streams/{event_stream_id}

You can have up to 5 active event streams per credential and 20 per account. Beyond that, POST returns 429 too_many_streams. The stream filters must fall within the credential's events.read scope. An unknown event type returns 422 validation_failed for filters.event_types.

Create, retrieve, and revoke

POST /event-streams
Idempotency-Key: 01J8Z3...
Content-Type: application/json

{"filters": {"event_types": ["product.created", "product.updated"], "resource_types": ["product"]}, "expires_in_hours": 24}
{
  "data": {
    "object": "event_stream",
    "id": "str_01J8Z3...",
    "status": "active",
    "filters": {"event_types": ["product.created", "product.updated"], "resource_types": ["product"]},
    "cursor": {"last_event_id": null, "advanced_at": null},
    "connection": {"connected": false, "since": null},
    "expires_at": "2026-09-24T12:00:00+00:00",
    "last_seen_at": null,
    "created_at": "2026-09-23T12:00:00+00:00"
  }
}

expires_in_hours ranges from 1 to 24 and defaults to 24. GET /event-streams lists the credential's event streams, with cursor and limit working as in any other collection (see Pagination and filtering). GET /event-streams/{event_stream_id} returns a single event stream. DELETE /event-streams/{event_stream_id} sets the stream to revoked and closes any open connection. An event stream created by another credential returns 404 not_found, even within the same account. status is active, revoked, or expired.

SSE transport

GET /event-streams/str_01J8Z3.../events
Authorization: Bearer ck_live_...
Accept: text/event-stream
Last-Event-ID: evt_01J8Z2...
  • Authenticate with the Authorization header only. Credentials in the query string are not accepted.
  • The response is 200 with Content-Type: text/event-stream, Cache-Control: no-cache, and X-Accel-Buffering: no. The body starts with retry: 3000 and contains one block per event:
id: evt_01J8Z3...
event: product.updated
data: {"object":"event","id":"evt_01J8Z3...","type":"product.updated", ...}

: keep-alive
  • data is exactly the JSON returned by GET /events/{event_id}. id is the event's public ID, which you can send back in Last-Event-ID.
  • Cursor: without Last-Event-ID, the connection resumes from the cursor stored on the event stream. With the header, it resumes after that event, without redelivering what was already sent. If that event is past the 30-day retention period, the API returns 410 cursor_expired: start over from GET /events.
  • After each delivered batch, the server advances cursor.last_event_id and last_seen_at. A : keep-alive comment is sent every 15 seconds.
  • One connection per event stream: a second concurrent connection receives 409 stream_busy. If a client disappears without closing its connection, the connection is released after 25 minutes and the next connection is accepted normally. Closing the connection releases it immediately.
  • Each connection extends the event stream's expiration to 24 hours from that moment.
  • The server closes the connection after 25 minutes, and also whenever the event stream expires, is revoked, is taken over by another connection, or the credential is revoked. Reconnect with Last-Event-ID to continue.
  • Expect a latency of a few seconds: the server checks for new events every second, and events become visible after the same delay of about 3 seconds that applies to GET /events. Delivery is not real-time.
  • Audit trail: one entry is recorded when a connection opens. Delivered events are not recorded individually.
ResponseWhen
409 state_conflictThe event stream is revoked or expired.
409 stream_busyAnother connection is already open.
410 cursor_expiredLast-Event-ID refers to an event that is no longer retained.
404 not_foundThe event stream does not exist or belongs to another credential.
403 permission_deniedThe credential lacks event_streams.read.

Event streams past their expiration time move to expired automatically within about a minute, and abandoned connections are released at the same time. The internal events event_stream.created, event_stream.revoked, and event_stream.expired are visible only with audit.read.

Test scenarios in the sandbox

Test scenarios are available only in a sandbox account. Outside it, the API returns 409 sandbox_required. Each scenario has an ID with the scn_ prefix. See the Sandbox guide and the Sandbox reference for details.

  • GET /sandbox/test-scenarios and GET /sandbox/test-scenarios/{scenario_id} (sandbox.read) return {object: "test_scenario", id, name, description, params{}, effects[], version}. params is the schema of the variables the scenario accepts, and effects lists what the scenario creates, the events it emits, and the simulated emails.
  • POST /sandbox/test-scenarios/{scenario_id}/runs (sandbox.manage, Idempotency-Key required, optional body {"params": {...}}) creates a sandbox.run_scenario operation (a single item that cannot be retried) and returns 202. The scenario runs in the sandbox account, every record it creates has meta.simulated = true, and the resulting public events (product.created, user.created, ...) reach webhooks and event streams just as they would in production. The item result contains {created: {product: 2, user: 3, ...}, events_emitted: n}.

Repeating the same Idempotency-Key returns the same operation without running the scenario again. A sandbox reset (POST /sandbox/resets) deletes everything the scenarios created.

On this page