Projects for people, streams for routing
A project is a folder (a client, a niche). Projects never route. A stream is the routing object: it selects forms by id or by tag (tag:vending, project:prj_…), owns a versioned output schema, and carries a mapping per form. Routes hang off the stream.
curl -X POST https://postbag.dev/v1/streams -H "Authorization: Bearer pb_live_…" -d '{
"name": "Vending leads", "slug": "vending-leads",
"schema": { "json_schema": { "type": "object", "required": ["name","phone"],
"properties": { "name": {"type":"string"}, "company": {"type":"string"}, "phone": {"type":"string"}, "message": {"type":"string"}, "site": {"type":"string"} } } },
"sources": [ { "selector": "tag:vending", "mapping": { "name": {"from":"fullName"}, "phone": {"from":"tel"}, "site": {"const":"fleet"} } } ]
}' curl -X POST https://postbag.dev/v1/streams -H "Authorization: Bearer pb_live_…" -d '{
"name": "Vending leads", "slug": "vending-leads",
"schema": { "json_schema": { "type": "object", "required": ["name","phone"],
"properties": { "name": {"type":"string"}, "company": {"type":"string"}, "phone": {"type":"string"}, "message": {"type":"string"}, "site": {"type":"string"} } } },
"sources": [ { "selector": "tag:vending", "mapping": { "name": {"from":"fullName"}, "phone": {"from":"tel"}, "site": {"const":"fleet"} } } ]
}' One partner, one window, one digest
A route from the stream to the partner's signed webhook with window { from, until } delivers only inside the campaign. A second route to the ops inbox in digest mode sends one email at 08:00 Europe/Stockholm with the day's leads. Both are rules on the route, not code in fifteen sites.
The sixteenth site
POST /v1/forms with from_template: st_… creates a form pre-attached to the stream with a valid mapping and a managed schema served at /s/{id}/schema. A site factory, or the agent building the site, cannot produce a form the stream does not understand. This is how Postbag's own site fleet is provisioned.
Change without surprise
When a site adds a field, drift shows up on the form. When the partner wants a new field, publish stream schema v2; every mapping is re-validated and stream.schema.changed goes out to anyone subscribed. Deliveries carry the schema version they conform to.