# Numbers

Source: https://jointextkit.com/docs/numbers
Index: https://jointextkit.com/llms.txt

## 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](/docs/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](/docs/compliance) 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"
}
```

| 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 (toll-free and Canadian; see below).

### Toll-free numbers

A toll-free number (800, 833, 844, 855, 866, 877, 888) is not registered: it
is **verified** with the carriers for one business and one use case. Until
its verification is approved it texts your verified numbers only, like a
`test_only` local number; once approved it texts anyone who opted in, and
its texts and STOP/HELP replies name the verified business. You can buy one
before your account has live access: add credit, buy it, then verify it in
the console (Numbers → Toll-free verification): you give your legal
business name, EIN, registered address and what you send, and we fill in the
rest. When we know your website, we read its public pages (with the fixed
rules of [Fill in from your website](/docs/compliance#fill-in-from-your-website))
and fill in your legal name, brand and address where they are still
blank, marked for you to check, and use your own privacy policy and terms
(we never write them for you; the [website check](/docs/compliance#what-the-website-check-looks-for)
says what they need); **Fill from
website** reads it again. We also fill in
you as the contact, from your account; a description of your program;
the volume; the HELP and STOP replies, example messages, your website, and,
when you have no registration page of your own, a
[hosted opt-in page](/docs/compliance#hosted-opt-in-page) that links your own
terms and privacy policy. Everything we filled in stays editable. When you
submit, you sign for the business the same way as for a
[registration](/docs/compliance) (once per business: your name, your title
and a short statement that you can act for it). Our team checks the form,
usually the same business day, then files it with the carriers, with a
screenshot of your opt-in page, and the carriers write to our support
address, not yours: we answer what we can and forward anything only you can
answer. You are told when it is approved (usually days, sometimes weeks).
When the carriers approve it, that also verifies the business with us (no
separate [business review](/docs/compliance#business-verification) to wait
for), and an account that had no live access goes live.

**Sending something else.** An approved verification can't be changed. To
send what a number isn't verified for (marketing from a number verified for
codes, say), add another toll-free number: its form starts as a copy of the
verified one, you pick the new use case, and the first number keeps its
approval.

**Before your account is live.** Once you own a live toll-free number you
can create a **live API key** without waiting for live access. Until the
account goes live, live sends (`POST /v1/messages`, and the console's
Send message) go out **only from that toll-free number and only to your
verified numbers**, billed from your credit like any live message; a send
from another number, or to anyone else, answers `403 sender_not_registered`
and is not charged. Such a key can also read messages, numbers, your
balance and usage; every other live endpoint answers `403
live_access_required` until the account is live, and then the same key
works everywhere.

Each live toll-free number carries its verification:

```json
"toll_free": {
  "status": "verified",
  "business": "Example Dental",
  "use_case": "Appointment reminders",
  "verified_at": "2026-10-20T16:00:00.000Z"
}
```

`status` is `not_started`, `pending`, `verified` or `rejected`.
`toll_free` is `null` on every other number. Canadian numbers cannot be
registered through us yet and text your verified numbers only.

`status` is `pending` from when you submit the form until the carriers
answer. Toll-free numbers need no 10DLC registration or registration fees,
and their `sender` is `null`, but the carriers verify **each** toll-free
number: a number you buy later gets a form of its own, filled in as a copy of
your last one. A local number is the other way round: once its registration
is approved, up to 49 numbers join it with no new review.

### Toll-free verification by API

Platforms can verify their users' toll-free numbers without the console. It
is the same form, filled in the same way, and it goes to the same review.

1. `POST /v1/toll-free-verifications` with the numbers and anything you
   know (legal name, EIN, address, use case). We fill in the rest and return
   a `draft` with `problems`: what the carriers would send it back for.
2. `PATCH /v1/toll-free-verifications/{id}` until `ready` is `true`.
3. `POST /v1/toll-free-verifications/{id}/submit`, with the business
   attestation the first time for a business. It comes back `in_review`.

```bash
curl -X POST https://api.jointextkit.com/v1/toll-free-verifications \
  -H "Authorization: Bearer $TEXTKIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"numbers":["+18335550100"],"business_name":"Acme Plumbing LLC","ein":"12-3456789","street":"1 Main St","city":"Springfield","state":"IL","postal_code":"62701","use_case":"customer_care"}'
```

| `status` | What it means | Who acts |
| --- | --- | --- |
| `draft` | Not submitted | You |
| `in_review` | A person on our team is checking it, usually within a business day. Nothing has gone to the carriers yet; you can still change it | Us |
| `submitted` | We sent it to the carriers. Usually days, sometimes weeks | The carriers |
| `verified` | Its numbers text anyone who opted in | Nobody |
| `rejected` | Sent back. `rejection.reason` says why: fix it with `PATCH`, then submit again | You |

Every form is checked by a person before it goes to the carriers; the API
never sends one straight there. Poll `GET /v1/toll-free-verifications/{id}`
for the answer; we also email you when it is verified or sent back. The
calls need a key with the `registrations` scope. A verification can't change once it is with
the carriers or verified.

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` when the registration is
approved. A number bought later links on its own, usually within 1 business
day. Each change emits
`number.sender_updated` (`data`: `phone_number`, `state`,
`previous_state`, `registration_id`, `reason`).

If our team ever records an approval by mistake (nothing was filed with the
carriers), we undo it: the registration goes back to our filing queue, or back
to you with a reason, and its numbers leave `active` (`pending` or
`action_needed`, with the same event and an entry in your
[audit log](/docs/audit-logs)).

A send to anyone else from a number that is not `active` answers **403**
`sender_not_registered` with an `X-TextKit-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.

## Request a short code

Short codes aren't bought on the spot: carriers approve each one, in 8 to 12
weeks. File the request and our team sets it up with you by email:

```bash
curl -X POST https://api.jointextkit.com/v1/numbers/requests \
  -H "Authorization: Bearer ssms_sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "type": "short_code", "business_name": "Acme Inc", "monthly_volume": "over_100k",
        "use_case": "Order and delivery updates to customers who opted in at checkout." }'
```

`monthly_volume` is `under_1k`, `1k_10k`, `10k_100k` or `over_100k`. One
open request per type; asking again updates it. `GET /v1/numbers/requests`
lists the open ones. Short-code carrier fees are passed through at cost, on top
of the per-segment rate. The console's **Add a number** has the same form.

## 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.

### Replacing a filtered number

If you release a live number that carriers were filtering as spam and buy
another within 7 days, new live numbers are put on hold for your account:
purchases answer `403 number_purchase_held` until our team has reviewed
it, and the owner and admins are emailed. Your existing numbers keep
working. Carriers treat swapping a filtered number for a fresh one as
evading their filters ("number cycling") and fine it; a new number doesn't
fix what got the old one filtered, so look at the wording, the opt-in and
the volume instead.

## 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:

```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 TextKit 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.jointextkit.com/v1/numbers/pools \
  -H "Authorization: Bearer $TEXTKIT_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.

### Local numbers

Create or patch a pool with `"routing": "local"` and a recipient gets a
member in their area code when the pool has one, else one in their time zone,
else any member. It is still the same number for that recipient every time.
The default, `"sticky"`, ignores location.

## Throughput

`GET /v1/numbers/:id` returns `throughput`: the messages a minute
TextKit holds the number to.

```json
"throughput": { "per_minute": 240, "shared_by": "registration", "registration_id": "reg_..." }
```

The pace belongs to the number's registration and is shared by every number
linked to it: 240 a minute, 75 for a low-volume registration. A direct send
over it gets `429 rate_limited` with Retry-After; batches and scheduled
messages wait for the next minute instead. `null` means no TextKit pace
applies (the number is not linked to a registration); carrier limits still do.

## 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.
