> ## Documentation Index
> Fetch the complete documentation index at: https://internal.softcrum.com/llms.txt
> Use this file to discover all available pages before exploring further.

# SMS Adapter

> After this ships, a tenant can send an OTP or a delivery alert by SMS in the countries where they are registered to do so.

## Context

SMS is the channel with the widest reach in Latin America and the most operational friction of any
we support. It differs from every other adapter in three ways that make it its own delivery:

**It needs a new vendor**, which means an ADR before the first import (DEC-E1). **It costs real
money per message**, so a runaway loop is a bill rather than a graph. And **sender registration is
per country** — alphanumeric sender IDs, short codes and long codes each have their own rules per
market, and sending without registration means silent non-delivery rather than an error.

The channel row and the port have existed since day one (FS-MSG-0001). Only the adapter is missing,
which is exactly what DEC-E1 intended.

## Scope *(normative)*

* The SMS adapter implementing `NotificationChannelPort`.
* `messaging.sms_sender_registrations`: per tenant, per country.
* Per-message cost tracking, feeding metering.
* Segment counting and encoding awareness (GSM-7 versus UCS-2).
* Delivery receipts mapped into the standard status vocabulary.
* A hard per-tenant spend cap, independent of the message quota.

## Non-scope *(normative)*

* Inbound SMS and two-way conversation. Different product.
* WhatsApp — F2, and a different channel entirely despite sharing a phone number.
* Choosing the provider. That is the ADR this feature is blocked on.

## Behaviour *(normative)*

1. **Blocked until the provider ADR exists** (DEC-E1). No SMS vendor is imported before it.
2. Sending to a country where the tenant has no verified sender registration is **rejected with a
   typed error**, not attempted. An unregistered send is silently dropped by carriers, which is the
   worst failure mode: it looks like it worked.
3. Phone numbers are normalized to **E.164** before sending, and an invalid number fails terminally
   and suppresses.
4. Message length is computed with **encoding awareness**: a single accented character switches the
   message to UCS-2 and halves the segment size. The console shows segment count before sending,
   because a tenant who thinks they sent one message and paid for three will not think it was fair.
5. Cost per message is **recorded per send** and fed to metering. SMS is the one channel where a bug
   costs money in real time.
6. A **hard per-tenant spend cap** applies, independent of the message quota, and it stops sending
   rather than billing an overage.
7. Delivery receipts map into the standard status vocabulary. Where the provider offers no receipt,
   the send stops at `sent` and the limitation is documented rather than faked as `delivered`.
8. SMS is available for the **transactional rail only in F1b**. Marketing SMS carries per-country
   consent requirements stricter than email, and it waits for that to be specified.

## Data *(normative)*

| Table                                | Key invariants                                                                                                                                       |
| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `messaging.sms_sender_registrations` | (`tenant_id`, `country_code`); `sender_type` alphanumeric\|short\_code\|long\_code; `sender_id`; `status` pending\|verified\|rejected; `verified_at` |

## API *(normative)*

| Endpoint                                   | Class      | Permission                       | Budget    |
| ------------------------------------------ | ---------- | -------------------------------- | --------- |
| `GET/POST /v1/messaging/sms-registrations` | Management | `messaging.sms.{read\|register}` | p95 \<1 s |

## Events *(normative)*

None beyond the standard send status events.

## Acceptance criteria *(normative)*

1. No SMS vendor is imported until its ADR is accepted — enforced by the lockfile allowlist.
2. Sending to an unregistered country is rejected with a typed error and no provider call.
3. Numbers are normalized to E.164; an invalid one fails terminally and suppresses.
4. Segment count is correct for GSM-7 and for UCS-2, proven with fixtures including an accented
   character.
5. Cost is recorded per send and appears in the usage snapshot.
6. The spend cap stops sending, and does not bill an overage.
7. A provider without delivery receipts leaves the send at `sent`, never a fabricated `delivered`.
8. **Negative:** no marketing-category message is dispatched on SMS in F1b.

## Execution

Blocked on the provider ADR. Once accepted: an adapter file plus a catalog row, with no change to
the dispatcher — which is the point of the port.

## Open questions

| # | Question                                                                       | Decides | By                            |
| - | ------------------------------------------------------------------------------ | ------- | ----------------------------- |
| 1 | Which provider, and does it cover every country we support?                    | Daniel  | the ADR this FS is blocked on |
| 2 | Is SMS included in a plan or always pay-as-you-go, given the per-message cost? | Daniel  | before commercial launch      |

## Changelog

| Version | Date       | Change        | Why | Author                 |
| ------- | ---------- | ------------- | --- | ---------------------- |
| 0.1.0   | 2026-08-17 | Initial draft | —   | daniel + claude-opus-5 |

## Delivery record

*Not implemented yet.*
