Numbers

Search available numbers

GET /v1/numbers/available?area_code=415

Returns up to 5 available numbers. In the sandbox this is a deterministic fake inventory; with live access it searches real US/Canada inventory across 200+ area codes.

Purchase a number

POST /v1/numbers with { "phone_number": "+1..." } (optionally "customer_id": "cus_..." to assign it to one of your customers)

Adds the number to your account (quota applies: default 2 live numbers, 3 sandbox). Returns the Number object. Emits a number.purchased event. Send an Idempotency-Key header and a retried purchase returns the original 201. Without one, the retry gets "You already own this number."

List your numbers

GET /v1/numbers (?customer_id=cus_... for one customer's). Each number carries customer_id (null when unassigned) and sender (below). GET /v1/numbers/+14155550132 returns one.

Sender status

A live US number works the moment you buy it, with one restriction: until it is registered with the carriers it is test only. It can text your verified numbers (your own phone, and any number you verify under Billing → Verified numbers), and nobody else. That is enough to build and test your integration end to end while the registration is in review. Once the number is linked to an approved registration it can text anyone.

Every number carries its standing:

json
"sender": {
  "state": "pending",
  "registration_id": "reg_a1B2c3D4e5F6",
  "reason": null,
  "updated_at": "2026-10-04T16:20:00.000Z"
}
StateMeaningCan text
test_onlyNo registration has been submitted for itYour verified numbers
pendingA registration is in carrier review, or approved and being linkedYour verified numbers
activeLinked to an approved registrationAnyone
action_neededSomething needs you: the registration was rejected, its website check fails, or the link failed. reason says exactly what to doYour verified numbers

sender is null on sandbox numbers (they never reach a real phone) and on numbers that need no registration.

You do not have to do anything to move a number along. Submit one registration and every live number on the account follows it: pending while the carriers review, then linked and active after approval, usually within minutes. A number bought later links on its own. Each change emits number.sender_updated (data: phone_number, state, previous_state, registration_id, reason).

A send to anyone else from a number that is not active answers 403 sender_not_registered with an X-SimpleSMS-Sender-State header and a message that says what to do. Nothing is sent and nothing is charged. Broadcasts, scheduled sends and automations follow the same rule: those recipients fail with sender_not_registered, never silently.

Choose a registration for a number

POST /v1/numbers/+14155550132/registration with { "registration_id": "reg_..." }

Only needed when your account has more than one approved registration (the number is action_needed until you choose), or to retry a link that failed. You can also pass registration_id when you buy the number. One registration holds up to 49 numbers, the most carriers allow; attaching a 50th answers 409. Returns the number with its new sender.

Assign a number to a customer

PATCH /v1/numbers/+15005550132 with { "customer_id": "cus_..." }, or { "customer_id": null } to unassign. Messages to and from the number are attributed to that customer from then on; earlier messages keep theirs.

Release a number

DELETE /v1/numbers/+15005550132

Marks the number released (live mode removes it from its registration, then disconnects it at the carrier). Rate limited to 10 releases per 30 minutes. Emits number.released.

Live number purchase and release require live access; sandbox numbers work for everyone immediately.

Buy in bulk

POST /v1/numbers/bulk-purchase

json
{ "area_code": "415", "quantity": 10, "pool_id": "pool_a1B2c3D4e5F6", "tags": ["spring"] }

Up to 50 per call. Your number limit is checked for the whole quantity first: if it doesn't fit you get 429 quota_exceeded saying how many more you can add, and nothing is bought. After that, a number the carrier refuses is listed in failed and replaced from spare inventory; shortfall is how many the area code couldn't supply. Test keys mint sandbox numbers.

Tags, labels, and bulk actions

Every number carries an optional label ("Front desk") and up to 20 tags (lowercased). Filter on them anywhere: GET /v1/numbers?tag=spring&pool=pool_...&mode=live&q=front.

POST /v1/numbers/bulk applies one action to up to 1000 numbers:

actionExtra field
add_tags / remove_tagstags: [...]
set_labellabel (null clears)
add_to_pool / remove_from_poolpool_id
releaseconfirm: "RELEASE <count>"

The response is always one result per id you sent, so a batch where 3 of 143 ids were wrong tells you which 3:

json
{
  "object": "bulk_result", "action": "add_tags",
  "requested": 143, "succeeded": 140, "failed": 3,
  "results": [{ "id": "+14155550132", "ok": true }, { "id": "+1415555", "ok": false, "error": "invalid_id" }]
}

Per-id errors: invalid_id, duplicate, not_found, too_many_tags, mode_mismatch (a test key releasing a live number), carrier_error.

Release is guarded. Up to 100 per call, and confirm must be "RELEASE <number of ids sent>", so a script that built the wrong list fails before anything is disconnected. Released live numbers go back to carrier inventory and may not be recoverable.

Sender pools

A pool is a named group of your numbers. Send with "from": "pool_..." and SimpleSMS picks a member, the same one for each recipient every time, so replies stay in one thread on their phone. Live keys draw only live members; test keys only sandbox members.

bash
curl https://api.joinsimplesms.com/v1/numbers/pools \
  -H "Authorization: Bearer $SIMPLESMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Support"}'

GET/PATCH/DELETE /v1/numbers/pools/:id, and POST /v1/numbers/pools/:id/clone copies a pool (same description and members, new id). Numbers can sit in several pools. Deleting a pool keeps its numbers. Membership is set with the bulk actions above.

Export

GET /v1/numbers/export returns CSV (phone_number, mode, label, tags, pools, pool_ids, created_at) and takes the same filters as the list. The console exports a filter or an exact selection.