Pombity

by Aric Software & Design

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.

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:

Refusals a sending application should branch on
CodeMeaningWhat to do
invalid_api_keyMissing, wrong, or revoked keyFix configuration; do not retry.
quota_exceededMonthly cap reached (cap, used, resetsAt, upgradeUrl in the body)Stop until resetsAt, or upgrade.
velocity_limit_exceededToo many recipients in the window (HTTP 429)Wait Retry-After seconds, then retry.
destination_not_verifiedFree-tier live send to an unverified numberRun the verify flow, or use a sandbox key.
unsupported_region / verification_limit_reached / destination_limit_reachedVerification anti-abuse limitsRespect the limit; do not retry today.
content_refusedBody failed the carrier content screenRewrite the message; do not resubmit verbatim.
account_suspendedThe account cannot sendContact the operator; do not retry.
idempotency_conflict / idempotency_in_flightSame key, different body / original still runningFix the key reuse; wait and re-read.
sandbox_key_requiredSandbox-only endpoint called with a live keyUse the ak_test_ key.
missing_field / invalid_phone_number / body_or_template / field_too_longRequest validationFix the request.
provider_errorUpstream carrier/provider failureRetry 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: