# Changelog

Source: https://joinsimplesms.com/changelog
Index: https://joinsimplesms.com/llms.txt

## 2026-10-05: Prepaid credits

Billing is now prepaid. See [Credits](/docs/credits).

- **Add credits** in Console → Billing: $10, $20, $50, $100, or any amount
  from $5 to $1,000. Your first purchase lifts the free-tier caps.
- **`402 insufficient_credits`**: a billable live call your balance cannot
  cover is refused and not charged. The body carries `balance_micro_usd`,
  `required_micro_usd` and `top_up_url`. Applies to sending, verification,
  lookups and buying numbers. Sandbox and the free tier are unaffected.
- **`GET /v1/balance`** returns your balance and auto-recharge setting
  (`balance.get()` in the Node and Python SDKs, 2.3.0).
- **Auto-recharge**, off by default: when the balance falls below an amount
  you choose, add an amount you choose from your saved card.
- **Events**: `credits.low`, `credits.depleted`, `credits.topped_up`
  and `credits.auto_recharge_failed`, each once per episode. Low,
  depleted and declined-card notices are also emailed to the account's owner
  and admins, and so is a confirmation each time funds are added.
- **Phone numbers** are charged when you buy them and monthly after that,
  not prorated. A number whose fee cannot be covered is kept, and outbound
  is paused until you add credits.
- **Credit history** in Billing, and as CSV (`GET /v1/exports/credit_history`).
- Monthly statements itemise usage and are paid from your credits.

## 2026-10-05: What the rate includes, stated once

We said two different things about fees in two places. This is the policy,
and the pricing page, the docs and the [Terms](/terms) (section 4.3) now all
say it the same way.

- **Carrier fees are included.** The per-message fees carriers charge are
  inside the published rate. There is no surcharge line, and that has not
  changed.
- **Registration fees are separate.** We file your A2P 10DLC registration for
  you, as before. The registry's brand, campaign and vetting fees, and
  toll-free verification fees, are passed through at cost with no markup.
  Earlier pages described registration as included at no extra charge; that
  wording is gone.
- Fines or penalties a carrier, registry or regulator imposes because of your
  traffic or registration details remain yours, as the Terms already said.

## 2026-10-05: Recurring schedules, and a sandbox that says what it did

- **Recurring schedules**: `POST /v1/schedules` sends a message every day,
  week or month at a local time in an IANA time zone, correct across daylight
  saving. List, retrieve, change, pause and resume (`PATCH` with
  `active`), delete. Each occurrence is a normal send, checked against
  opt-outs, quota, your spend limit and your credit balance when it fires, and a normal message
  with `schedule_id` (`GET /v1/messages?schedule_id=`). A schedule pauses
  itself after 3 opt-out skips in a row; an occurrence we reach more than an
  hour late is skipped, never sent late. In both SDKs (2.2.0) as `schedules`, and on
  the console's Scheduled page. See [Recurring schedules](/docs/schedules).
- **Webhook events**: `schedule.occurrence_skipped`, `schedule.paused`.
- **`simulated`**: every message, and every `message.sent`,
  `message.delivered` and `message.failed` event, now says whether anything
  was handed to a carrier. A test-key send is simulated (`true`) unless it
  goes to your own verified phone. A simulated send to a real-looking number
  also answers with a `notice`, and test-key sends carry an
  `X-SimpleSMS-Simulated` header. Statuses are unchanged.
- **`from` is optional** on `POST /v1/messages` when the account has one
  number for the key's mode: your sandbox number with a test key, your one
  live number with a live key. Otherwise the error lists the choices.
- **`scheduled_message`** objects now carry `scheduled_at`, matching the
  request field. `run_at` stays as a deprecated alias.
  `GET /v1/scheduled_messages/{id}` exists (it answered 405).
- **Sign-up** ends with an optional step to verify your phone. Skipping it, or
  a code that cannot be sent, never blocks the account.

## 2026-10-05: Log in and test webhooks from the terminal

- **`npx joinsimplesms login`** opens the console for you to approve and
  stores a new test key for the machine, then prints your sandbox number. No
  key to copy. Test keys only; `login --key` still stores one you have.
  See [CLI](/docs/cli).
- **`npx joinsimplesms listen --forward-to http://localhost:3000/webhooks`**
  replays your events to a local server, signed like real webhooks with a
  secret it prints. No tunnel. Your configured endpoints are not touched. See
  [Test locally](/docs/webhooks#test-locally).
- **`npx joinsimplesms trigger message.received`** fires a sandbox event.
- **Webhook endpoints over the API**: `POST /v1/webhooks` (returns the
  signing secret once), `GET` / `PATCH` / `DELETE /v1/webhooks/{id}`,
  and `webhooks.create / get / update / delete` in the Node and Python
  SDKs (2.1.0). Restricted keys need the new `webhooks:write` scope; the
  existing `webhooks` scope stays read-only.
- **`GET /v1/events/wait`**: long-poll for new events (`events.wait` in
  the SDKs).
- **JSON errors on the API host**: an unknown path on
  `api.joinsimplesms.com` now answers `404` with
  `{"error":{"code":"not_found",...}}` instead of an HTML page, and an
  unsupported method answers `405` `method_not_allowed` with an
  `Allow` header instead of an empty body.

## 2026-10-05: Opt-outs you can see

- **`recipient_opted_out`**: a send to an opted-out number now answers
  `403` with its own code (it was the generic `forbidden`) and says when
  and how the person opted out, in the message and as `opted_out_at`,
  `opted_out_via` and `opted_out_method`. `failure.code` is still
  `opted_out`. If you matched on `forbidden` for this case, match the new
  code.
- **`message.blocked`**: every refused send emits an event and webhook, is
  noted in the number's consent history, and is counted on
  `GET /v1/consent/{phone}` (`blocked_sends`, `last_blocked_at`).
- **`message.help_requested`**: emitted when a recipient texts HELP.
- **Replies in your name**: STOP, START and HELP replies now name your
  business, and a registered number sends exactly the replies its
  registration filed, with your support contact. They used to name Delivered.
- **START is confirmed**: someone who was opted out gets one "you are
  resubscribed" reply. A repeat STOP no longer sends a second confirmation.
- `consent.get()` is also available as `contacts.consent(phone)` in the
  Node and Python SDKs.
- **The suppression list fails closed**: if it cannot be read, a send
  answers `503` with `Retry-After` instead of going out unchecked.

## 2026-10-05: Delivered is now SimpleSMS

The product has a new name and a new home, **joinsimplesms.com**. It is the
same service, the same account, the same prices and the same terms; nothing
you built has to change. Apart from the API host, everything issued under the Delivered name
keeps working, with no end date:

- **API host: this one changes.** `api.deliveredsms.com` and
  `mcp.deliveredsms.com` are retired and answer `410 host_retired` with
  the new host in the body. Point your base URL at
  `https://api.joinsimplesms.com/v1` (MCP: `https://mcp.joinsimplesms.com`).
  Paths, request shapes and your key are unchanged.
- **API keys.** `dsms_sk_...` keys (and `resms_sk_...`)
  authenticate forever. Only newly created keys get the new `ssms_sk_`
  prefix.
- **Webhook headers.** Every delivery carries `simplesms-signature` /
  `simplesms-event-id` and, with identical values, `dsms-signature` /
  `dsms-event-id` (and the `resms-` pair). Signing secrets are unchanged.
- **Response header.** `X-Delivered-Sender-State` is still sent, alongside
  `X-SimpleSMS-Sender-State`.
- **Node package.** `npm install deliveredsms` still works: it re-exports
  the new `joinsimplesms` package, and `Delivered` / `DeliveredError` are
  aliases of `SimpleSMS` / `SimpleSMSError` (the same classes).
- **CLI.** `deliveredsms` and `dsms` still run; the new command is
  `simplesms`. A key saved in `~/.deliveredsms.json` is still read.
- **Environment variables.** The SDKs and CLI read `SIMPLESMS_API_KEY` and
  `SIMPLESMS_BASE_URL` first, then `DELIVERED_API_KEY` and
  `DELIVERED_BASE_URL`.
- **Agent skills.** `/skills/delivered` and `/skills/delivered-verify`
  redirect to `/skills/simplesms` and `/skills/simplesms-verify`.
- **Your data.** Numbers, webhook endpoints, registrations, opt-outs and
  message history were not migrated or re-created. They are where they were.

What does change, and when you would notice:

- **Texts we send for you name SimpleSMS.** The Verify template when you send no `app_name`
  (`SimpleSMS code: 482193...`), and the sandbox suffix on texts to your own
  phone. Your own message bodies and your own `app_name` are untouched.
- **Webhook requests identify as `SimpleSMS-Webhooks/1.0`** in
  `User-Agent` (was `Delivered-Webhooks/1.0`). Verify the signature, not the
  user agent.
- **The console lives at joinsimplesms.com.** You sign in with the same
  account; the first visit to the new address asks you to sign in once.

To move over at your own pace: point your base URL at
`api.joinsimplesms.com`, install `joinsimplesms`, rename `Delivered` to
`SimpleSMS`, and read `simplesms-signature` in your webhook handler. None
of it is required.

## 2026-10-04: Sender status

- **Numbers show where they stand.** Every number carries `sender`
  (`test_only`, `pending`, `active`, `action_needed` with a plain
  reason). New `GET /v1/numbers/{number}`, and the
  `number.sender_updated` event. See [Numbers](/docs/numbers#sender-status).
- **Test only until registered.** A live US number that is not linked to an
  approved registration can text your verified numbers and nobody else. Other
  sends answer `403 sender_not_registered` with
  `X-Delivered-Sender-State`. This replaces the
  `X-Delivered-Registration` / `X-Delivered-Warning` headers, which are
  gone: unregistered traffic is now stopped instead of warned about.
- **Automatic linking.** When a registration is approved, the account's live
  numbers are linked to it and become `active`; numbers bought later link
  on their own. `POST /v1/numbers/{number}/registration` chooses a
  registration explicitly (up to 49 numbers each).
- **One form to go live.** Submitting a registration from a sandbox account
  is also the live-access request. Submit now requires complete business
  details and answers `400` naming what is missing.

## 2026-10-04: Automations

- **Event-driven flows**: `POST /v1/track` (`events.track` in the SDKs)
  records what a person did; a flow listening for that event sends, waits,
  waits for another event with a timeout, and branches. See
  [Automations](/docs/automations).
- **`/v1/automations`**: create, edit, activate, pause, and read runs with
  a step-by-step timeline. New key scope `automations`.
- **Webhook events**: `automation.run.started`, `automation.run.completed`,
  `automation.run.failed`.
- **Console**: Automations, with three starter templates and a "Send a test
  event" button that works in the sandbox.

## 2026-10-04: Contacts API

- **`/v1/contacts`**: create-or-update by phone number, retrieve, update,
  delete, and list with `tag` and `phone_number` filters. The same
  address book the console uses. See [Contacts](/docs/contacts).
- **`POST /v1/contacts/import`**: bulk upsert, up to 500 per request, with
  skipped rows reported by index.
- **`POST /v1/contacts/bulk`**: add tags, remove tags, or delete for up to
  500 contacts in one call.
- New key scope `contacts`. `contacts` resource in the Node and Python
  SDKs.

## 2026-10-01: Message observability

- **Message timeline**: every message records each step with a timestamp
  (`accepted` → `validated` → `queued` → `sent_to_carrier` →
  `carrier_accepted` → `delivered`/`failed`; inbound: `received`) as
  `timeline` on `GET /v1/messages/{id}`.
- **`segments`, `encoding`, `price`, `destination_carrier`** on every message.
- **Readable failures**: failed messages, `message.failed` events, and
  refused sends (opt-out, content block, rate limit, spend limit) carry a
  `failure` object with a stable code, a plain explanation, and what to do.
  See [delivery failures](/docs/errors#delivery-failures).
- **List filters**: `status`, `direction`, `to`, `from`, `created_after`,
  `created_before` (alongside `customer_id`) on `GET /v1/messages`, with
  full pages. Automatic carrier retries show up on the timeline per attempt.
- **Sandbox numbers** for every failure, opt-out, rate limiting, and a really
  delayed delivery. The happy-path number now settles to `delivered` instead of
  staying `sent`.
- **Idempotency-Key** now applies to scheduled sends too.
- **Console**: message filters and a message inspector (timeline latencies,
  cost, failure, related webhook events).

## 2026-10-01: Reliability

- **Automatic carrier retries**: temporary carrier failures no longer fail a
  send. `POST /v1/messages` answers `202` with the message `queued` and we
  retry it after 30s, 2m and 10m. New message fields: `attempts`,
  `next_attempt_at`. A message is never submitted twice.
- **Missing delivery reports**: a sent message with no delivery report after
  72 hours is marked `receipt_status: "missing"`. Its status stays `sent`.
- **Idempotency-Key** on `POST /v1/verify` and `POST /v1/numbers`. Failed
  requests now release their key.
- **Webhook delivery log**: status code, latency, attempt and the first 2 KB of
  your endpoint's response for every attempt, in the console and at
  `GET /v1/webhooks/deliveries`. `GET /v1/webhooks` lists endpoints.
- **Replay to one endpoint**: `POST /v1/events/{id}/replay`, and per-row
  Replay in the console.
- **Undelivered events**: events whose retries ran out are kept for 30 days
  with one-click replay, instead of being dropped.
- **[Status page](/status)** (and `/status.json`) for the API, sending,
  inbound and webhooks.

## 2026-10-01: Customers and the Python SDK

- **Customers** (`/v1/customers`): for platforms sending on behalf of other
  businesses. Assign numbers to a customer and its messages, verifications,
  events and webhooks carry `customer_id`; `GET /v1/customers/{id}/usage`
  reports per-customer usage by day. See [Customers](/docs/customers).
- `customer_id` on `POST /v1/messages`, `POST /v1/verify`,
  `POST /v1/numbers`, new `PATCH /v1/numbers/{number}`, and filters on
  `GET /v1/messages` and `GET /v1/numbers`. Numbers now always include
  `customer_id` (`null` when unassigned).
- **Python SDK**: `pip install joinsimplesms`. No dependencies, typed,
  retries, idempotent sends, webhook verification.
- **Node SDK**: consent and customers resources, `listAll()` iterators, and
  `verifyWebhook()`.

## 2026-10-01: Compliance autopilot (registration)

- **Website disclosure check**: `POST /v1/registrations` checks your website,
  privacy policy, terms and opt-in page against what carrier reviewers look for,
  with evidence and copy-paste fixes for every finding.
- **Generated copy**: opt-in disclosure, privacy clause, SMS terms, samples,
  HELP/STOP replies, and the full brand + campaign draft.
- **Registration workflow**: recheck, submit, carrier review, rejections with
  plain-language reasons and fixes, resubmission; `registration.updated`
  events. See [Compliance & registration](/docs/compliance).
- **Opt-in proof**: `proof` on `POST /v1/consent/{phone}` and
  `/v1/consent/export?type=ledger`.
- Live sends without an approved registration carry an
  `X-Delivered-Registration` warning header. Nothing is blocked.

## 2026-10-01: Account controls

- **Deliverability**: `GET /v1/deliverability` and Console → Deliverability
  report delivery rates by day, carrier, and sending number, with failure
  reasons. Sharp drops raise a `deliverability.degraded` webhook event.
- **Spend limits**: a monthly USD cap on live messaging with alert thresholds
  (`GET`/`PATCH /v1/spend-limit`). Sends past the cap return
  `spend_limit_reached` and are not charged; `spend.threshold_reached`
  fires at each threshold.
- **Audit log**: key, team, webhook, spend-limit, number, consent, and
  settings changes, with actor and IP. Console → Audit log and
  `GET /v1/audit-logs`.
- **Key scopes**: restrict a key to the endpoint families it needs; others
  answer `403 insufficient_scope`. Existing keys keep full access.
- **Viewer role**: read-only teammates.

## 2026-09-30: Spam scores retired

- `GET /v1/lookup/{phone}/spam` now answers **410 Gone** (`endpoint_retired`).
  Its scores came from data SimpleSMS no longer uses. `GET /v1/lookup/{phone}`
  (line type and carrier) is unchanged. The MCP `lookup_spam` tool and the CLI's `--spam` flag are gone; the
  SDK's `lookup.spam()` is deprecated and throws `endpoint_retired`.

## 2026-09-22: Pricing

- **Outbound SMS is $0.009 per message**, all-in. Carrier fees
  are still included and A2P 10DLC registration is still free; the rate simply
  now covers what a US text costs to deliver. Verification, numbers, lookups,
  and the free tier are unchanged.

## 2026-08-14: Consent autopilot (TCPA)

- **Revocation in plain English**: "please stop texting me" now opts a number
  out, not just the STOP keyword. Three detection tiers (keyword, phrase, AI
  with confidence scores), per the FCC's April 2025 reasonable-means rule.
- **Consent ledger**: every opt-out, opt-in, import and verification exemption
  is appended to a per-number history that is never deleted.
- **Consent API**: `GET/POST /v1/consent/{phone}`, paginated
  `GET /v1/consent`, bulk `/v1/consent/import` (up to 500), and
  CSV `/v1/consent/export`.
- **Console Compliance page**: suppression list with per-number history,
  import, export, and the verification-exemption audit log.
- `message.opted_out` events now carry `method` and (for AI
  detections) `confidence`; `verification.sent_to_opted_out` is
  now subscribable as a webhook event.

## 2026-08-06: Early access launch

- **Sandbox-first API surface**: `/v1/messages` (send, get, list, Idempotency-Key
  support), `/v1/numbers` (search, purchase, release), `/v1/lookup` (+`/spam`),
  `/v1/events`, and `POST /v1/test/inbound` for simulating inbound SMS.
- **Self-serve console** at [/console](/console): instant
  free sandbox keys, no card.
- **Live mode** (after live-access review): real number provisioning across 200+
  US/Canada area codes and real SMS delivery.
- **Agent surface**: OpenAPI ([yaml](/api/v1/openapi.yaml) ·
  [json](/api/v1/openapi.json)), `llms.txt`, single-file docs
  (`llms-full.txt`), markdown twins of every docs page, an
  [MCP server](/.well-known/mcp.json), and an agent skill
  ([simplesms](/skills/simplesms/SKILL.md)).

### Known limitations

- Live delivery receipts are not yet emitted; a live message's status stays
  `sent` (sandbox simulates the full lifecycle). Webhook endpoints shipped
  2026-08-13.
