Transactional channels
Open in
Transactional channels carry what a Customer bought, what changed on their records and their next move, over email, SMS, chat, voice and in-app. Every message in or out lands in one conversation per record.
Filled: what the Customer sees. Hollow: a step inside the product, or a tool outside it.
Capability families
A channel is one more Interface onto the same record. A reply by SMS and a call over the API reach the same gate. An email the product sends is the Event a Watch receives, rendered for a person.
| Family | What it gives every product |
|---|---|
| Delivery sends | What was bought, sent: an edition to members, a filed report, a receipt, a file. Once per entitled recipient, from your Domain, with each delivery's state |
| Notifications | A record the recipient is party to has moved: a draft is ready, a booking changed, a payment failed. Sent on the Event that moved it |
| A turn by message | A notification that carries the recipient's next move (sign, approve, accept), made by a reply, a single-use link, or the same Verb over the API |
| Inbound intake | An address, a number or a chat entry mapped to a Verb. Each message is an Event and runs through the gate like a call: a new case, or a reply in an open one |
| Conversations | One thread per record across email, SMS, chat, voice, in-app, the API and MCP. Every send and reply is kept with the record, answers search, and exports with it |
| Templates | Each message's wording, generated from the file in your brand and the recipient's language, and changed by a diff |
| Preferences and consent | Per principal and channel: what a purchase covers, what needs an opt-in, quiet hours and digests. An opt-out suppresses the address everywhere |
| Sending identity | Your Domain and numbers, each checked once, with bounces and complaints handled before they harm the next send |
| Delivery state and fallback | Per message and recipient: queued, sent, delivered or failed, retried, then sent on another channel the recipient allows |
| Voice | An inbound call answered, transcribed into the thread and routed like a message; an outbound call only as a delivery or a notification |
The line between delivery and campaigns
One rule draws it: a message that delivers what a Customer bought, or answers a record they are party to, is product. A message aimed at someone who did neither is a campaign.
| A channel delivers | A campaign | |
|---|---|---|
| Why it is sent | The recipient bought, called, or is party to the record | The recipient is on a list or in a segment |
| What starts it | An Event on that record | A schedule or an audience |
| Where it is kept | The record's conversation | Outside the product |
| What permits it | The purchase, or an opt-in on that channel | Consent and rules of its own |
| For example | An edition to members, "your draft is ready to sign", a receipt, a booking reminder | A newsletter to people who never joined, a win-back, a nurture sequence, a launch announcement |
A campaign tool reads the product's Events across the seam: every send, open, reply, bounce and opt-out. It sends under its own consent, never through a product's conversation.
Lifecycles and states
| Thing | States | What moves it |
|---|---|---|
| A conversation | open, waiting_on_customer, waiting_on_seller, closed | A delivery, an inbound message or a Verb opens it; it closes with its record |
| A message out | queued, sent, delivered, then opened or replied; or bounced, failed, suppressed | The channel's answer, the recipient, and the suppression list |
| A message in | received, matched, then attached or opened (a new case); or refused, with a reply that says why | The address or number it reached, the sender's principal, and the gate |
| Consent, per principal and channel | required, opted_in, opted_out | A purchase covers delivery; SMS and voice need an opt-in; the recipient changes either by reply, in the portal, or by a Verb |
| A sending identity | added, verifying, verified, suspended | One DNS record or number check; a complaint rate past the limit suspends it |
| A template | Follows the Version it ships in | A diff, checked at deploy |
A refused inbound message still gets an answer: the sender learns which Verb was refused and its cure, in the channel it wrote from.
What you declare, what the platform does
| You declare | The platform does |
|---|---|
A thread on a Noun, with its channels | Opens one conversation per record, routes every channel into it, and keeps every message with the record |
notify on a state: who, what it says, the move it offers | Sends on the Event, in the channel the recipient prefers, with a link or a reply that makes the move |
A send move with to | Sends once to each recipient the plan entitles, keeps each delivery's state, and stops on a bounce |
An inbound address or number, and the Verb it starts | Writes each message as an Event, runs the Verb through the gate, and attaches the reply to its thread |
| Your Domain and numbers | Checks each once, sends from them, and handles bounces and complaints |
| Nothing for consent | Holds what each purchase covers, asks for opt-ins where a channel needs one, honours opt-outs and quiet hours, and refuses at deploy any send with no record its recipient is party to |
Example
filings keeps one conversation per Filing, tells the Customer when it is their turn, and opens a Filing from an email.
import { define, workflow, agentic } from 'apis.dev'
/** Each Customer's annual report, drafted, signed and filed, charged once when the Customer accepts it. */
export default define({
name: 'filings', domain: 'filings.apis.dev',
Filing: {
company: 'string', year: 'number',
status: {
order: [], draft: { in: 'ordered', run: workflow(async ($, filing) => { … }) },
sign: { in: 'drafted', floor: 'signature' }, file: { in: 'signed', run: agentic('File the signed report with the state') },
accept: 'filed', reject: 'filed',
},
thread: { channels: ['email', 'sms', 'chat', 'voice'] }, // one conversation per Filing
notify: {
drafted: { to: 'customer', say: 'Your {year} report is ready to sign', act: 'sign' },
filed: { to: 'customer', say: 'Filed. Accept it within 7 days', act: ['accept', 'reject'] },
},
},
inbound: { 'orders@filings.apis.dev': 'Filing.order' }, // an email opens a Filing
offer: {
'Filing.order': { price: 25, when: 'accepted', accept: 'customer',
acceptWithin: '7d', remedy: 'redo', grade: 'measured' },
},
})The Filing's conversation as an agent reads it: the order by email, the turn by email and SMS, and a reply by text.
{
"id": "conv_1",
"record": "filing_1",
"status": "waiting_on_seller",
"messages": [
{ "id": "msg_1", "dir": "in", "channel": "email", "from": "dana@acme.com",
"event": "Filing.ordered", "at": "2026-10-24T09: 00: 00Z" },
{ "id": "msg_2", "dir": "out", "channel": "email", "kind": "notification",
"event": "Filing.drafted", "act": "Filing.sign", "state": "delivered",
"at": "2026-10-25T10: 02: 00Z" },
{ "id": "msg_3", "dir": "out", "channel": "sms", "kind": "notification",
"event": "Filing.drafted", "act": "Filing.sign", "state": "delivered",
"at": "2026-10-25T10: 02: 00Z" },
{ "id": "msg_4", "dir": "in", "channel": "sms", "from": "dana@acme.com",
"text": "Can I sign tomorrow morning? ", "at": "2026-10-25T10: 14: 00Z" }
],
"next": null
}A send aimed past the line fails the check before it deploys.
$ npx apis.dev check
✕ notify 'every Mon 09: 00' → everyone: a send needs a record its recipient bought, called or is party to.
A campaign runs outside the product, on its Events.Contracts with the other subsystems
Channels never decide. An inbound message is a call, and an outbound one is an effect of a commit.
| Subsystem | What channels take from it | What channels give it |
|---|---|---|
| Definition and compile | thread, notify, inbound and templates, checked at deploy: a send with no party to it is refused | Templates as versioned records, changed by a diff |
| Identity, access and policy | The sender's principal, parties without accounts, single-use links for a turn | Consent and preferences as grants, per principal and channel |
| Model and records | The record a conversation belongs to | Conversations and messages as records, searched and exported with it |
| Data and Sources | A Customer's own inbox and number, connected as an Integration | Replies written back to the system they came from |
| Runtime | The Event that starts each send; quiet hours and digests as clocks | Each send as a durable effect, sent once; each inbound message as an Event |
| Work | The case an inbound message opens, and its turn | Replies as input to the case; an agent's delegated sending in the thread |
| Commerce | Entitlements: who a delivery is owed to; receipts and invoices to send | Delivery of what was bought, recorded against the purchase |
| Interfaces and distribution | Your Domain and brand; the in-app channel through the UI contracts | The conversation as one more Interface onto the record |
| Trust and operations | Suppression, complaint limits, residency | The evidence of every send and every consent |
What Customers and their agents meet
- One thread, every channel. An email reply, a text and an API call about the same case land in one conversation, in order.
- Their turn arrives with its move. "Ready to sign" carries a link that signs; an agent makes the same move with one Verb.
- No message they did not earn. A Customer gets what it bought and what its records do, and nothing it never asked for.
- Preferences that hold. A Customer switches SMS off in the portal, by replying STOP, or by a Verb. Every channel honours it at once.
- Answers to what they send. An inbound message runs as a call. It opens a case or replies in one, and a refusal comes back with its cure.
- An agent reads the thread.
GET /{noun}/gives an agent the whole exchange. It replies through the API, and the person sees the reply in email.{id}/ conversation
Every message is about a record someone bought or is party to, so the product's channels carry only what was earned.
The site, the portal and the docs
The three products every launch includes: one that sells, one that runs each account and one that teaches, generated from one file under your brand.
UI contracts and the headless-parity test
What any UI renders, generated from the file, and a test that an agent finishes every path a UI offers with no UI.