Skip to content

Routing: many forms, one shape, any destination

A route sends a form, or a whole stream of forms, to a destination with rules. Streams are how an operator turns fifteen slightly different contact forms into one contract a partner can rely on.

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.

POST /v1/routes
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.

mapping
{
  "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.

Questions, answered

Can one form go to several destinations?
Yes. Create several routes from the same form, or attach the form to a stream that has several routes. Each (submission, route) pair becomes exactly one delivery.
What happens to fields the stream schema does not have?
They are kept under extras in the delivered payload. Nothing is dropped silently.
Can a route be limited to a date range?
Yes. Set window.from and/or window.until. Submissions outside the window produce a skipped delivery with reason window.
Do digests respect my timezone?
Yes. A digest route carries a cron expression and an IANA timezone; the default business timezone is the organization's setting.

Your first form is three minutes away.

Sign up, get a submit URL, point a form at it. The first submission lands in your inbox and your email. Everything else appears when you need it.