Postbox docs

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]"}
FieldMeaning
idThe stream cursor. Monotonic. This is what makes resume work.
eventThe event type, identical to the webhook X-Postbox-Event.
dataJSON 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-0

Or as a query param, which is what you need in a browser, since EventSource cannot set headers:

GET /v1/events?lastEventId=1784215248111-0

The 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.bounced

Comma-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

SSEWebhooks
DeliveryWhile connected, resumable by cursorDurable, retried
DirectionYou connect outWe connect in
Needs a public URLNoYes
Good forLive UI, local development, agents waiting on a replyServer-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.