---
title: "Transactional channels"
url: /interfaces/channels
---

# Transactional channels

> Email, SMS, chat and voice that deliver what was bought and answer what was asked, in one conversation per record, with consent per channel.

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.

**Transactional channels: in, through the record, out.**

- Email, SMS, chat and voice come in to intake, which runs a Verb through the gate into the record's one conversation.
- A move on the record writes an Event. The send step checks consent and entitlement, then sends on the channels the recipient allows, and every send stays in the thread.
- Below the seam, campaigns read Events only. A message with no purchase and no call runs outside the product.

## Capability families
A channel is one more [Interface](/every-product.md) 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](/interfaces.md#families) | 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 every send, open, reply, bounce and opt-out as Events, and sends under its own consent.

## 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](/reference/platform.md#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
A `thread` on a Noun names its channels, and every channel routes into one conversation per record. `notify` on a state says who hears, what it says and the move it offers. A send move with `to` reaches each recipient the plan entitles, once, and an `inbound` address or number names the Verb it starts.

Consent needs nothing declared, and deploy refuses 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.

```ts title="definition.ts"
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) => {
        const { form } = await $.generative('Draft the annual report for {company}', filing,
          { returns: { form: 'string' } })
        await $.human('Check the draft', { form }, { role: 'paralegal' })
        return { form }
      }) },
      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.

```json title="GET /filings/filing_1/conversation"
{
  "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.

```sh title="Terminal"
$ 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.

- **Definition and compile** checks `thread`, `notify`, `inbound` and templates at deploy, and keeps templates as versioned records.
- **Identity, access and policy** names the sender's principal, sends single-use links for a turn, and holds consent and preferences as grants.
- **Model and records** keeps conversations and messages with their record, searched and exported with it.
- **Data and Sources** connects a Customer's own inbox and number as an [Integration](/patterns/headless-saas.md#integrations), and writes replies back to it.
- **Runtime** starts each send on its Event, runs quiet hours and digests as clocks, and sends each message once as a durable effect.
- **Work** opens a case from an inbound message and takes replies as input; an agent's delegated sending stays in the thread.
- **Commerce** says who a delivery is owed to through [Entitlements](/reference/platform.md#entitlement), and hands over receipts and invoices to send.
- **Interfaces and distribution** gives your Domain, brand and the in-app channel.
- **Trust and operations** applies suppression, complaint limits and residency, and keeps the evidence of every send and 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.
- **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.
- **An agent reads the thread.** `GET /{noun}/{id}/conversation` gives an agent the whole exchange. It replies through the API, and the person sees the reply in email.

---

Every message is about a record someone bought or is party to, so the product's channels carry only what was earned.