Events (SSE)
Stream events live over Server-Sent Events, with resume-from-cursor so a reconnect loses nothing.
GET /v1/events is a Server-Sent Events stream carrying the same events as
webhooks, with the same payloads. Use it to drive a live UI. Use
webhooks for durable server-side work.
import { Postbox } from "@postbox/sdk";
const pb = new Postbox({
apiKey: process.env.PB_KEY!,
projectId: process.env.PROJECT_ID!,
});
for await (const event of pb.events.stream()) {
console.log(event.event, event.data);
}import os
from postbox import Postbox
pb = Postbox(api_key=os.environ["PB_KEY"], project_id=os.environ["PROJECT_ID"])
for event in pb.events.stream():
print(event.event, event.data)using Postbox;
var pb = new PostboxClient(
new PostboxOptions
{
ApiKey = Environment.GetEnvironmentVariable("PB_KEY")!,
ProjectId = Environment.GetEnvironmentVariable("PROJECT_ID")!,
}
);
await foreach (var e in pb.StreamAsync())
Console.WriteLine($"{e.Event} {e.Data}");import cloud.postboxapp.*;
import cloud.postboxapp.gen.*;
import java.util.*;
Postbox pb = new Postbox(PostboxOptions.builder().apiKey(System.getenv("PB_KEY")).projectId(System.getenv("PROJECT_ID")).build());
pb.stream(event -> System.out.println(event.event()));import postbox "github.com/postboxapp/postbox-go"
pb, err := postbox.NewWithKey(os.Getenv("PB_KEY"))
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
pb.Stream(ctx, func(e postbox.Event) error { fmt.Println(e.Event); return nil })curl -N https://api.postboxapp.cloud/v1/events \
-H "Authorization: Bearer $PB_KEY"The stream is tenant-scoped. A credential only ever receives its own tenant's events.
Framing
retry: 3000
id: 1784215248111-0
event: message.received
data: {"mailboxId":"mbx_...","messageId":"msg_...","from":"[email protected]"}| Field | Meaning |
|---|---|
id | The stream cursor. Monotonic. This is what makes resume work. |
event | The event type, identical to the webhook X-Postbox-Event. |
data | JSON payload, always a single line. |
Resuming without gaps
On reconnect, send the last id you processed and the stream replays everything strictly after it:
Last-Event-ID: 1784215248111-0Or as a query param, which is what you need in a browser, since EventSource
cannot set headers:
GET /v1/events?lastEventId=1784215248111-0The cursor is a server-side position in a per-tenant event log, not a client-side estimate. A reconnect inside the retention window loses nothing.
Browser EventSource sends Last-Event-ID automatically on reconnect, using
the last id: it saw. If you track the cursor yourself, persist it only after
you have finished processing that event, not when you receive it.
Filtering
GET /v1/events?types=message.received,message.bouncedComma-separated, exact names, no wildcards. Filtering happens server-side, so it saves bandwidth rather than just tidying your handler.
Heartbeats
The server emits a keepalive comment every 15 seconds while idle. That exists so proxies and load balancers do not silently kill a quiet connection. Ignore the comments; any SSE client already does.
The retry: 3000 directive tells the client to reconnect after 3 seconds if the
connection drops.
In the SDK
events.stream() gives you the event stream as an async iterator, with
reconnection handled for you:
import { Postbox } from "@postbox/sdk";
const pb = new Postbox({ apiKey: process.env.PB_KEY! });
for await (const event of pb.events.stream({ types: "message.received" })) {
console.log(event.event, event.data);
}Pass an AbortSignal to stop:
const controller = new AbortController();
setTimeout(() => controller.abort(), 60_000);
for await (const event of pb.events.stream(undefined, controller.signal)) {
// ...
}The client tracks the cursor and reconnects for you.
Choosing between SSE and webhooks
| SSE | Webhooks | |
|---|---|---|
| Delivery | While connected, resumable by cursor | Durable, retried |
| Direction | You connect out | We connect in |
| Needs a public URL | No | Yes |
| Good for | Live UI, local development, agents waiting on a reply | Server-side work that must not be missed |
For local development, SSE is usually the faster path: no tunnel, no public URL, no signature verification to get wrong.