Skip to main content
Webhooks push events to your server. The real-time event stream pushes the same events to your frontend: one long-lived GET that receives every event as it happens, with guaranteed replay if the connection drops. Use the stream to keep a dashboard live (balances, payins landing, payouts settling, card authorizations, KYT alerts for admins). Use webhooks for anything that must survive your browser being closed — the two channels carry the same events with the same payloads and the same event_id, so you can consume both without writing two mappings.

Open the stream

The endpoint requires the same Authorization: Bearer credential as the rest of the API, so the browser’s native EventSource (which cannot send headers) is not an option. Use fetch with a streaming reader:
Response
Every event frame carries three lines:

Reconnect without gaps

If the connection drops, reconnect sending the last cursor you processed. The server replays everything you missed from the log before switching back to live, so a flaky network never loses an event.
The replay is capped at 1,000 events. If you were offline long enough to miss more, the stream emits a replay_truncated control event and you should reconcile with ?snapshot=true or with GET /v1/events/history instead of assuming continuity.

Initial snapshot

Opening with ?snapshot=true sends the current state first, then the deltas. It removes the classic race of “read the REST endpoints, then subscribe, and lose whatever happened in between”: the cursor is taken after the subscription is open, so nothing falls through the crack.
The snapshot is absolute state, never deltas — applying it twice is harmless. It contains balances for an account credential (same fields as GET /v1/balances) and, for an organization admin, the operational health counters.

Filter by event type

?types= narrows what you receive. It can only restrict what your credential already sees, never widen it. An unknown type is rejected with 400 invalid_event_type instead of silently returning nothing.

Scope: account vs organization admin

An account never sees another account’s events, and never sees the org-wide compliance surface. The stream never delivers a field that the same credential could not already read over REST.

Control events

Besides your business events, the stream emits protocol events. They carry no id: (except snapshot), so they never move your replay cursor. A : ping comment arrives every 20 seconds to keep proxies from closing an idle connection — ignore lines starting with :.

Limits

Exceeding the concurrency limit returns 429 too_many_streams. Exceeding the openings quota returns 429 rate_limited — that one counts attempts, so a client reconnecting in a tight loop burns it even with no stream open. Always honour the retry: hint (3 s) plus exponential backoff.

Queryable history

The same log that feeds the stream is readable over REST — useful for auditing, for a “what did I miss” view, or when the replay was truncated.
A single event by its public id:
The event log keeps 90 days. It is a notification buffer, not the financial record: balances, payins, payouts, transfers and ledger entries are immutable and stay available for as long as regulation requires through their own endpoints and the statement.

Errors

Full list in Errors.

FAQ

No. The stream lives as long as the browser tab; webhooks reach your backend even when nobody is looking. Use the stream for the UI and webhooks for anything that triggers business logic (reconciliation, accounting, notifications).
Yes — after a reconnect the replay may re-deliver the boundary event, and both channels (webhook and stream) share the same event_id. Deduplicate by event_id and treat every payload as absolute state.
By design. A stream with no lifetime cap hides leaked connections. The server sends a reconnect control event first, and reconnecting with Last-Event-ID continues exactly where you were.
No. The stream needs no configuration: it delivers everything your credential is allowed to see. Webhook subscriptions only control HTTP deliveries to your server.
Check that your HTTP client is not buffering the response (in fetch, read res.body as a stream instead of awaiting res.text()), and that no proxy of yours is buffering text/event-stream. CBPay already disables buffering on its side.
Yes, with ?account_id=. The filter only works inside your own organization; anything else returns 404.
Last modified on July 26, 2026