The compliance layer
These rules run inside the service, on every send, from every door: HTTP, MCP, and CLI alike. Do not re-implement them in your application, and do not try to route around them. A refusal or a held message is the rule working; every refusal carries a machine-readable code so you can branch without parsing prose.
Consent ledger
The service keeps the consent ledger: a durable, append-first record of every opt-in and opt-out, with its source (texted keyword, texted YES, web form, verification code, import attestation). A send to a number with no opt-in on record, or with an opt-out, yields a Blocked row and no carrier traffic. That is correct behavior, not an error to retry. Check standing first with GET /v1/consents (or the check_consent MCP tool) if you need to know before composing.
STOP and HELP
STOP, START, and HELP are handled by the service on every number it sends from, exactly as the carriers require. A STOP suppresses the number immediately and is mirrored into the ledger; HELP answers with the service identity and contact. Never send your own opt-out instructions, and never re-text a number after a STOP; the service will block you, and it is right to.
Quiet hours and the response rule
Non-emergency texts are held overnight in the recipient's local time (default 20:00–08:00). Two narrow, audited exceptions exist. A response, a single-recipient send within 15 minutes of the recipient's own action (their inbound text, or a web action your application reports via POST /v1/resident-actions), goes out at night; and a send to the key holder's own self-verified number is never held (see verified destinations). A Blocked or held result at night is the rule working; do not retry in a loop.
Verified destinations
On the free tier, live texts deliver only to destinations the key holder proved with a 6-digit code texted to that phone and typed back (POST /v1/verified-destinations, then /confirm). Anything else refuses whole-request with destination_not_verified. Confirmation writes self-verified consent to the ledger; an opt-out always outranks it. Anti-abuse limits, each with its own named refusal: US and Canada numbers only (unsupported_region), verification codes capped per tenant per day and per destination number per day (verification_limit_reached), at most 3 verified destinations per free tenant (destination_limit_reached). Codes expire in 10 minutes, 5 attempts.
Quotas and caps
Free is capped, never unmetered. Caps are per channel and per mode: live SMS, email, and sandbox SMS each have their own monthly cap (UTC month), and sandbox traffic never consumes a live cap. Crossing a cap refuses the whole request with:
{ "code": "quota_exceeded", "error": "…",
"cap": 25, "used": 25,
"resetsAt": "2026-12-01T00:00:00+00:00",
"upgradeUrl": "…" }
The numbers are honest, counted from the service's own message log. Do not retry before resetsAt; read your standing anytime with GET /v1/usage. Launch free-tier caps are on the pricing page and in pricing.json.
Velocity limits
A burst past the per-key sliding-window limit (default: 300 attempted recipients per 60 seconds) answers HTTP 429 with velocity_limit_exceeded and a Retry-After header. Back off exactly that long. A 429 is never stored as an idempotent replay, so your retry with the same Idempotency-Key is safe.
Content screen
Developer-tenant message bodies (and email subjects) pass a rules-based screen (SHAFT and carrier-prohibited categories, plus public URL shorteners), run on the resolved body, so merge fields cannot smuggle a term. A match refuses the whole request with content_refused and is audited. This is the carriers' floor, not an editorial opinion.
Suspension
A suspended account cannot send (SMS and email, live and sandbox), and every attempt answers account_suspended before anything is stored or sent. GET /v1/usage reports the standing. Reinstatement reverses it in one operation.
Sandbox mode
Sandbox is a property of the key (ak_test_…), not a different environment. Sandbox sends route to an in-process simulator with realistic Delivered receipts through the same delivery path as real carrier reports, and are flagged "sandbox": true on every row. Consent, STOP, quiet hours, quotas, velocity, and the content screen all run identically: the rules are the product, and the sandbox is where your agent learns them. POST /v1/sandbox/inbound (sandbox keys only) simulates an inbound text (a keyword, YES, STOP, HELP, or a plain sentence) so the full opt-in cycle is exercisable without a carrier. A simulated action never exempts a live night send.
Idempotency
Pass an Idempotency-Key header on POST /v1/messages and POST /v1/emails. A retry with the same key inside 24 hours returns the original response byte-for-byte and sends nothing. Use it in all retry logic; a different body under the same key answers 409 idempotency_conflict.
The error codes to branch on
Every error body carries code (machine, from one test-locked catalog, stable once published) beside error (human). The ones your send path should handle:
| Code | Meaning | What to do |
|---|---|---|
invalid_api_key | Missing, wrong, or revoked key | Fix configuration; do not retry. |
quota_exceeded | Monthly cap reached (cap, used, resetsAt, upgradeUrl in the body) | Stop until resetsAt, or upgrade. |
velocity_limit_exceeded | Too many recipients in the window (HTTP 429) | Wait Retry-After seconds, then retry. |
destination_not_verified | Free-tier live send to an unverified number | Run the verify flow, or use a sandbox key. |
unsupported_region / verification_limit_reached / destination_limit_reached | Verification anti-abuse limits | Respect the limit; do not retry today. |
content_refused | Body failed the carrier content screen | Rewrite the message; do not resubmit verbatim. |
account_suspended | The account cannot send | Contact the operator; do not retry. |
idempotency_conflict / idempotency_in_flight | Same key, different body / original still running | Fix the key reuse; wait and re-read. |
sandbox_key_required | Sandbox-only endpoint called with a live key | Use the ak_test_ key. |
missing_field / invalid_phone_number / body_or_template / field_too_long | Request validation | Fix the request. |
provider_error | Upstream carrier/provider failure | Retry with the same Idempotency-Key. |
The full catalog, with every code, lives in the OpenAPI document's description and in the API reference.
Opt-in flow documents of record
The complete opt-in flows (every message text verbatim, the expiries, attempt counts, and caps) are written up end to end in two documents of record, maintained beside the service's code and mirrored here:
- Developer traffic: the verified-destination flow on the developer number: the code text, confirmation, STOP and HELP, and the anti-pumping limits. This is the document submitted with that number's carrier verification.
- Community alerts (government): keyword opt-in, web sign-up with attestation and texted YES, CSV import with the consent request on first send, STOP and HELP, quiet hours and the response rule.