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 (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)
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
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 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 (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 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:
"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.
POST /v1/toll-free-verificationswith the numbers and anything you know (legal name, EIN, address, use case). We fill in the rest and return adraftwithproblems: what the carriers would send it back for.PATCH /v1/toll-free-verifications/{id}untilreadyistrue.POST /v1/toll-free-verifications/{id}/submit, with the business attestation the first time for a business. It comes backin_review.
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).
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:
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
{ "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:
{
"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.
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.
"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.