Messaging and consent
SMS opt-in and decline, the consent ledger, STOP and START handling, the per-patient message budget, the identity disclosure, and what a program must have before it can text anyone.
Nothing patient-facing is sent unless four things are true: a sender is configured for the environment, an active communication policy allowed the content, the address has consent open, and the patient's message allowance for the window has room. Each of those fails closed on its own, and each is recorded. This page covers consent and the budget; the gate covers content.
The sender
An SMS sender is configured per organization and environment under Settings, Notifications, or with POST /api/v1/notification-provider-configs (channel: "sms", provider, enabled, config, credentials), read with GET, changed with PATCH /{config_id} and removed with DELETE /{config_id}. There is no platform fallback: an environment with no enabled sender cannot send, and the readiness ladder reports it as the missing rung. Your provider account must point its inbound-message webhook at the URL the dashboard shows for your project (shape /webhooks/<sms-provider>/sms/{project_key}); that is how STOP reaches AnyBio.
Consent is per address, granted only by the patient
Consent is recorded per channel and address (a phone number), not per person, and its only sendable state is opted_in. The ledger has two parts: a gate row per address with its current status (opted_in or opted_out) and any outstanding invitation, and an append-only evidence trail of what was asked, what was shown, and what the patient replied.
Two rules are enforced by the types, not by convention:
- Nothing an organization does can grant consent. A form, an operator, or an API caller can only create a pending invitation. The grant is the patient's reply from the handset, verified against the carrier's signature.
- Only a patient reply can reopen a closed door. Once an address is
opted_out, the only path back toopted_inis the patient textingSTARTfrom that number. There is no override.
Asking: POST /api/v1/public/sms-optin
The public, unauthenticated endpoint an enrollment page posts to. Body: program_handle (the program's public handle, the one its QR code encodes), phone (any format that normalizes to E.164), captcha_token (from the widget on the enrollment page; required in practice).
It creates a pending invitation and sends the patient a message that carries the identity disclosure and asks them to reply START. It never grants. The invitation expires after 24 hours.
The response is deliberately uniform so the endpoint cannot be used to learn about a phone number: 400 for a string that is not a phone number or a destination the form does not text, 401 for a token no captcha issued, and otherwise 202 with the same body every time:
{ "status": "accepted" }A number that is already opted in, already opted out, never seen, over its rate limit, or attached to no program all receive that same 202.
Re-asking: POST /api/v1/communication_consent/sms/resend
Write scope. Body: { "address": "+15550100" }. Re-sends the invitation to an address the organization already asked, on the same per-handset budget as the public form, so a re-send is never a way around the cap. Returns invitation_id and correlation_id; it is not a delivery report.
The patient's reply
Inbound messages arrive at the per-project webhook, are verified against your provider account's signing token, and are classified by keyword:
| Reply | Effect |
|---|---|
STOP, STOPALL, UNSUBSCRIBE, CANCEL, END, QUIT | Consent for the address is revoked and any outstanding invitation withdrawn. A STOP with nothing to revoke is recorded as evidence and changes no state. |
START, UNSTOP | Confirms an outstanding invitation, granting consent under the disclosure version that was shown. With no invitation outstanding, restores a consent this address previously gave. Never manufactures consent for an address that was shown nothing. |
HELP, INFO | An informational reply; no state change. |
anything else, YES included | No consent change. The invitation asks for START, and only START confirms it. |
A message that is not a keyword is persisted durably, deduplicated in the database, attributed to the patient by the address the carrier signed, and handed to the symptom interview if one is open for that patient. Nothing is dropped between the webhook and the record, because the provider does not redeliver a message it has already been told was received.
The inbound webhook is rate-limited on its own bucket and that limiter fails open when its store is unavailable: a missed STOP is worse than an extra request.
The identity disclosure
The consent invitation carries the AI-identity disclosure and is stamped with a fixed version identifier (sms-v1.2 today), so every consent record names the exact wording the patient was shown. The composer refuses to produce an invitation when a party the document names (the organization, the program) has no usable name; the honest surface for that is a page that offers no form, not a message with a blank in it.
Each program with an agent also carries its own disclosure in spec.disclosure (identity, human_contact); the symptom interview's opening message includes it, and an adherence nudge establishes it once per episode before saying anything else. GET /api/v1/anybio/programs/{handle}/disclosure returns a program's disclosure for a public enrollment page to render.
The per-patient message budget
Automated care SMS is bounded by the environment's patient messaging policy: max_messages per window_seconds, set by an organization admin with PUT /api/v1/patient-messaging-policy (see care hand-offs). Every automated send reserves a slot before it reaches the wire:
- The policy counts logical messages, not carrier segments.
- The patient's sends across all their addresses, and the address's sends across all patient records, must both fit the same allowance.
- A reservation is committed before transport. An accepted or ambiguous send stays charged; only an established non-delivery releases the slot. A stale job cannot send a second copy after the window rolls over.
- Consent invitations and the replies to
HELPare service messages with their own controls and do not draw on this budget.
When the allowance is spent, or when no policy is configured, the send is not made and a care hand-off is opened for the patient with reason message_budget_exhausted or message_policy_missing, so a person owns the conversation from there. There is no numeric default; the numbers are agreed with the receiving team and written into the policy.
What a program needs before it can text
GET /api/v1/episode-profiles/{id}/symptom-readiness answers the program-scoped half of this list; the patient-scoped half is listed but not answered:
| Rung | Scope |
|---|---|
| A live project in the environment | program |
| A valid symptom protocol on the program, disclosure included | program |
| A rule that routes the reading to a symptom session | program |
| An enabled SMS sender for the organization and environment | program |
| An active communication policy | program |
| The patient exists and has an active episode that accepts the signal | patient |
A phone number on file (PUT /api/v1/xusers/{xuser_id}/phone) | patient |
| SMS consent open for that number | patient |
| No symptom session already open for the patient | patient |
What is recorded
Every step above leaves a row a reviewer can read without asking anyone: the invitation and its disclosure version, each inbound message with its classification, each consent grant and revocation with the address and the reply that caused it, each reservation against the budget and its settlement, and, for anything the gate touched, the decision record with the original and any rewritten content. Consent evidence is what the billing ledger reads to report consent_recorded_at per enrollment; see billing readiness.
Device ingest
The cellular device webhook (shared secret, per project), image ingest, waveform captures, and the provenance fields every observation carries - measured time versus received time.
Billing readiness
The fixed-period ledger of unique transmission days per enrollment, its tiers and pace states, the order reference, and the disclaimer that goes with all of it - evidence, never charges.