Streaming events
The Events API lets you page through the envelope log; the stream lets you follow it live. A single long-lived HTTP connection delivers every new envelope your filters match, as server-sent events, with a cursor on every frame so a dropped consumer resumes exactly where it left off. This page covers subscribing, filtering, cursors and resume tokens, delivery semantics, and working example consumers. The streaming endpoint is part of the v1.0 surface.
The stream endpoint
GET /v1/events/stream holds the connection open and writes one SSE frame per matching envelope. Authentication is the same Bearer key as every other route, and the stream is tenant-scoped by that key: you receive your tenant's envelopes and nothing else.
curl -N https://api.fibric.io/v1/events/stream?event_type=order.* \
-H "Authorization: Bearer sk_live_3f9c2a7b8e1d4f60a2c9" \
-H "Accept: text/event-stream"
Each frame carries the envelope as JSON, the event's id as the SSE id field, and a cursor inside the data. A comment frame is written periodically as a keep-alive, so idle streams survive intermediaries that reap silent connections.
id: ev_3a91c7
event: envelope
data: {"event_id":"ev_3a91c7","reseller_id":null,"tenant_id":"tn_9d40",
data: "workspace_id":null,"source":"magento","event_type":"order.updated",
data: "correlation_id":"co_51bb02","payload":{"order_id":"SO-10884","status":"processing"},
data: "agent_id":null,"session_id":null,"cursor":"cur_eyJpZCI6ImV2XzNhOTFjNyJ9"}
: keep-alive
Filtering
Filters are query parameters and combine with AND, the same filter vocabulary as GET /v1/events. Filtering server-side keeps the connection proportional to what you actually consume.
| Parameter | Type | Description |
|---|---|---|
event_type |
string · query | Exact type or glob, with the same semantics as operator triggers: * matches a single dot-delimited segment, so order.* matches order.created but not order.refund.issued. |
source |
string · query | Only envelopes from this source, for example magento or operator:jenny. |
workspace_id |
string · query | Only envelopes scoped to this workspace. Useful when workspaces separate environments. |
entity |
string · query | Only envelopes whose triggered plans touched this entity_key, the live counterpart of the list API's entity reconstruction. |
cursor |
string · query | Start position. Omitted, the stream starts at now; supplied, it replays forward from the cursor. See below. |
Cursors and resume tokens
Every frame carries a cursor: an opaque token naming a position in your tenant's event log. Persist the cursor of the last frame you fully processed, and you hold a resume token. There are two equivalent ways to hand it back:
- The
cursorquery parameter, for explicit resumption:GET /v1/events/stream?cursor=cur_eyJpZCI6…. The stream replays every matching envelope after that position, then continues live. - The
Last-Event-IDheader, which SSE clients such as the browser'sEventSourcesend automatically on reconnect, carrying theidof the last frame received. The server treats it as a cursor.
Cursors from the stream and next_cursor values from GET /v1/events name positions in the same log, which enables the standard backfill-then-tail pattern with no gap and no seam:
1. page GET /v1/events with your filters until has_more is false
2. keep the final next_cursor
3. open GET /v1/events/stream?cursor=<that cursor>
→ every event after your backfill, exactly in log order, then live
Cursors are opaque and query-bound: a cursor is valid for the filter set it was issued under, and presenting it with different filters fails with 400 invalid_cursor (Errors). A cursor older than the event retention window can no longer be resumed; restart with a backfill.
Delivery semantics
The stream inherits the platform's delivery model; it does not invent a stronger one.
- At-least-once. A reconnect resumes from your last acknowledged position, so frames after it may be delivered again. Deduplicate on
event_idif your processing is not naturally idempotent. - Ordered per log position. Frames arrive in log order for your filter set. Cross-entity ordering is log order, not causal order; per-entity action ordering is the executor's single-flight guarantee, not the stream's.
- Read-only. The stream is an observation surface. Consuming an envelope does not acknowledge, claim, or mutate anything; two consumers with the same filters both see everything.
- Tenant-scoped. The stream is bounded by the key's tenant, the same wall as every read; see Tenancy & isolation.
If what you are building is "when X happens, do Y against a business system", you likely want an operator, which gets the trust gate, single-flight, idempotency, and receipts for free. Streams are for the surrounding tissue: dashboards, data pipelines, notification fan-out, and your own monitoring.
Example consumers
Node, with resume and dedup
import { EventSource } from 'eventsource';
const seen = new Set<string>(); // dedup on event_id (bounded store in production)
let cursor = await loadCheckpoint(); // last durably processed cursor, or null
const url = new URL('https://api.fibric.io/v1/events/stream');
url.searchParams.set('event_type', 'order.*');
if (cursor) url.searchParams.set('cursor', cursor);
const es = new EventSource(url, {
headers: { Authorization: `Bearer ${process.env.FIBRIC_API_KEY}` },
});
es.addEventListener('envelope', async (frame) => {
const env = JSON.parse(frame.data);
if (seen.has(env.event_id)) return; // at-least-once: replays are expected
seen.add(env.event_id);
await process(env); // your handler
await saveCheckpoint(env.cursor); // checkpoint AFTER processing, not before
});
// EventSource reconnects automatically, sending Last-Event-ID as the resume token.
The checkpoint-after-processing order is what makes the consumer crash-safe: a crash between process and saveCheckpoint means the frame is delivered again on resume, and the event_id dedup absorbs it.
Shell, for inspection
# watch one entity's events as they happen
curl -N "https://api.fibric.io/v1/events/stream?entity=order:magento:SO-10884" \
-H "Authorization: Bearer $FIBRIC_API_KEY" \
-H "Accept: text/event-stream"
# the receipts-side equivalent, from the CLI
fibric receipts tail
Connection behavior and limits
- Opening a stream counts against the events route class in Rate limits & quotas; frames on an open stream do not consume request budget.
- The server may close a long-lived connection during a deploy. This is routine, not an error: reconnect with your resume token and nothing is lost. See Errors for 5xx handling.
- A consumer that stops reading has its connection closed after the buffer limit rather than degrading the tenant's stream fan-out; resume with the cursor when ready.
- If a stream appears healthy but silent, verify events are arriving at all before suspecting the stream; the diagnosis path is in Troubleshooting.