In short: In Postbag, a route is a link from a source (one form or one stream) to one destination, carrying rules: instant or digest mode, an optional delivery window, and quality filters. A stream is a named group of forms with a shared, versioned output schema; each form in a stream has a mapping from its own field names onto that schema.
Direct routes: the simple case
Most forms need one thing: an email when someone writes in. A direct route goes form → destination. The quickstart creates one for you; you can add a Telegram or webhook route to the same form at any time. A form can have any number of direct routes and can also belong to streams.
curl -X POST https://postbag.dev/v1/routes -H "Authorization: Bearer pb_live_…" -d '{
"form_id": "fm_8f3kq2",
"destination_id": "ds_7hm2q0",
"mode": { "type": "instant" },
"quality": { "exclude_spam": true, "exclude_quarantined": true }
}' curl -X POST https://postbag.dev/v1/routes -H "Authorization: Bearer pb_live_…" -d '{
"form_id": "fm_8f3kq2",
"destination_id": "ds_7hm2q0",
"mode": { "type": "instant" },
"quality": { "exclude_spam": true, "exclude_quarantined": true }
}' Streams: one output shape for many forms
A stream has a slug, a name and a current schema version. Forms attach to it either explicitly by id or by selector (tag:vending, project:prj_…). Each attachment, a stream source, carries a mapping.
The stream schema is the outbound contract: every route on the stream delivers payloads in that shape, stamped with the schema version. Changing it is a deliberate act that creates a new immutable version, emits stream.schema.changed, and re-validates every source mapping.
Mappings: direct, constant, default
A mapping says how a form's fields produce the stream's fields. Today it supports direct field references, constants and defaults; expression mappings (JSONata, ADR-005) are planned and currently return a clear expressions_not_enabled error rather than failing silently.
Unmapped form fields are kept under extras so nothing is lost. A mapping is valid or incomplete; an incomplete mapping (a required stream field with no source) blocks the attachment with a 422 that lists the missing fields, at creation time, to the dashboard and to the agent making the call.
{
"name": { "from": "fullName" },
"company": { "from": "Företag" },
"phone": { "from": "tel", "default": null },
"site": { "const": "kontorsautomat.se" }
} {
"name": { "from": "fullName" },
"company": { "from": "Företag" },
"phone": { "from": "tel", "default": null },
"site": { "const": "kontorsautomat.se" }
} Rules: windows, digests, quality
window: { from, until } timestamps. Outside the window a delivery is created with status skipped and reason window, so you can see that it happened and why.
mode: instant (default) or digest { cron, timezone }. Digest routes group a period's submissions into one delivery per destination, keyed by the unique (route, period) pair, so two workers can never send the same digest twice. Empty periods send nothing.
quality: exclude_spam and exclude_quarantined, both true by default. Turn them off for a route that should see everything, for example an audit webhook.
Preview before you commit
GET /v1/streams/{id}/preview renders recent submissions from each source through the current mappings, so you can see the mapped payload your partner will receive before a single delivery is sent.