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:
"sender": {
"state": "pending",
"registration_id": "reg_a1B2c3D4e5F6",
"reason": null,
"updated_at": "2026-10-04T16:20:00.000Z"
}| State | Meaning | Can text |
|---|---|---|
test_only | No registration has been submitted for it | Your verified numbers |
pending | A registration is in carrier review, or approved and being linked | Your verified numbers |
active | Linked to an approved registration | Anyone |
action_needed | Something needs you: the registration was rejected, its website check fails, or the link failed. reason says exactly what to do | Your 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
{ "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:
action | Extra field |
|---|---|
add_tags / remove_tags | tags: [...] |
set_label | label (null clears) |
add_to_pool / remove_from_pool | pool_id |
release | confirm: "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:
{
"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.
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.