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
| Permission | Operations |
|---|---|
event_streams.read | GET /event-streams, GET /event-streams/{event_stream_id}, GET /event-streams/{event_stream_id}/events |
event_streams.manage | POST /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
Authorizationheader only. Credentials in the query string are not accepted. - The response is
200withContent-Type: text/event-stream,Cache-Control: no-cache, andX-Accel-Buffering: no. The body starts withretry: 3000and contains one block per event:
id: evt_01J8Z3...
event: product.updated
data: {"object":"event","id":"evt_01J8Z3...","type":"product.updated", ...}
: keep-alivedatais exactly the JSON returned byGET /events/{event_id}.idis the event's public ID, which you can send back inLast-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 returns410 cursor_expired: start over fromGET /events. - After each delivered batch, the server advances
cursor.last_event_idandlast_seen_at. A: keep-alivecomment 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-IDto 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.
| Response | When |
|---|---|
409 state_conflict | The event stream is revoked or expired. |
409 stream_busy | Another connection is already open. |
410 cursor_expired | Last-Event-ID refers to an event that is no longer retained. |
404 not_found | The event stream does not exist or belongs to another credential. |
403 permission_denied | The 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-scenariosandGET /sandbox/test-scenarios/{scenario_id}(sandbox.read) return{object: "test_scenario", id, name, description, params{}, effects[], version}.paramsis the schema of the variables the scenario accepts, andeffectslists what the scenario creates, the events it emits, and the simulated emails.POST /sandbox/test-scenarios/{scenario_id}/runs(sandbox.manage,Idempotency-Keyrequired, optional body{"params": {...}}) creates asandbox.run_scenariooperation (a single item that cannot be retried) and returns202. The scenario runs in the sandbox account, every record it creates hasmeta.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.