A bulk SMS API lets you send the same text to a whole list with one request. On Ready, that request is POST /v1/messages/bulk with an array of numbers, a body and a consent flag. Sending safely means the provider does three things for you. It paces delivery to carrier limits, holds texts that would land during quiet hours, and refuses opted-out numbers before they cost you anything. Campaigns, drip campaigns and automations wait up to 24 hours for the window; a one-to-one or API text is held only if the window opens within 6 hours, otherwise it is refused with 409 QUIET_HOURS_BLOCK and must be resent; a one-to-one reply to someone who texted you in the last 24 hours goes out at any hour.
This post is for teams sending one message to a large list from code: a product announcement to opted-in users, a service notice to every customer in a region, a reminder to everyone booked this week. We'll cover the exact request, what happens between your API call and the recipient's phone, and the segment math that decides your bill. We make Ready, so the examples use our API. The pacing and segment rules come from the carriers, so they apply whoever you send through.
How the bulk SMS API call works
Here is the whole request:
curl -X POST https://api.tryready.com/v1/messages/bulk \
-H "X-API-Key: $READY_API_KEY" \
-H "Idempotency-Key: 3f6c2a9e-8d41-4b7a-9c55-1e2f0a7b6d13" \
-H "Content-Type: application/json" \
-d '{
"to": ["+12125550199", "+15559876543"],
"body": "Heads up: scheduled maintenance tonight 11 PM to 1 AM ET. Reply STOP to opt out.",
"consent_attested": true
}' Three parts matter.
tois an array of E.164 numbers. A 10-digit US number is treated as +1, but normalize anyway, so your dedupe step works on one format.consent_attested: trueis your promise that everyone in the array agreed to receive texts. We record that attestation per bulk send: who sent, to how many, and when. That gives you an audit trail of your own sending. It is not a per-contact consent record. Ready stores no recipient's opt-in time, wording or page URL, so your own database has to keep those. (A contact who signs up on a Ready-hosted opt-in page is the exception: Ready stores that sign-up, with the time, IP address and exact wording, and support@tryready.com can send you a copy.)Idempotency-Keyis any UUID. If your HTTP client times out and retries, the same key means the list doesn't get texted twice. A double send to 10,000 people means a second bill and a spike in STOP replies.
For large lists, you will probably split the list into batches in your own code. The bulk messages docs give the per-request limits. Give each batch its own idempotency key, and store that key next to the batch so a retry reuses it:
import { randomUUID } from "node:crypto";
async function sendBatch(numbers, body, key = randomUUID()) {
const res = await fetch("https://api.tryready.com/v1/messages/bulk", {
method: "POST",
headers: {
"X-API-Key": process.env.READY_API_KEY,
"Idempotency-Key": key, // persist this with the batch so retries reuse it
"Content-Type": "application/json",
},
body: JSON.stringify({ to: numbers, body, consent_attested: true }),
});
if (res.status === 429) { // faster than your carrier cap; nothing was sent
const { retry_after_ms = 1000 } = await res.json();
await new Promise((resolve) => setTimeout(resolve, retry_after_ms));
return sendBatch(numbers, body, key); // same key
}
if (!res.ok) { // 402 balance too low, 400 too many recipients; the body says what to do
const { code, error } = await res.json();
throw new Error(`${code}: ${error}`);
}
return res.json();
} We've kept response handling out of the snippet. A 200 can still include numbers we refused: data.failed counts them, and each entry in data.results has an error that says why, so log those entries. The messages API reference documents the response and the webhook events. Read message status back with GET /v1/messages, which pages with before_id. For the wider integration (auth, retries, webhook verification), our SMS API integration walk-through covers the plumbing in more depth.
Before you call it: what the list has to be
The API will send to whatever numbers you give it, within the refusals covered below. Most bulk-send problems start before the request.
- Consent first. Bulk SMS goes to people who opted in or who replied to you. Cold outreach belongs on the phone and in the mail. A cold list sent through a bulk endpoint is the fastest way to collect complaints, carrier filtering and TCPA exposure. Statutory damages run $500 to $1,500 per text.
- Normalize and dedupe. Convert every number to E.164 and drop duplicates. The same person stored as
(212) 555-0199and+12125550199gets texted twice otherwise, and you pay twice. - Validate if the list is old.
GET /v1/validate/:phonetells you whether a number can receive texts, at $0.003 for each number that gets an answer. You can also switch on landline validation for the account. That costs $0.003 per segment and is off by default. - Scrub if the list came from anywhere but your own signup flow.
GET /v1/scrub/:phonechecks a number against known TCPA-litigator and DNC-complainer lists. The standalone scrub costs $0.005 per contact. You can also switch on scrubbing for the account, which adds $0.003 per segment to sends and is off by default. - Check your registration.
GET /v1/registrationshows where your 10DLC registration stands. An unregistered account can send up to 500 texts a month, so a bulk list of any size hits that cap within a few calls.
Throughput: how carriers pace a big send
Your batches can add up to 30,000 segments. Each request takes at most 100 numbers. The carriers will not take them in one second. US carriers meter A2P 10DLC traffic in segments per second (TPS or MPS) per campaign. The bulk endpoint sends synchronously and checks your carrier throughput first: a batch that would go faster than your registration allows is refused with 429 throughput_exceeded, nothing in it is sent, and the answer carries retry_after_ms. Wait that long and send the same batch again with the same Idempotency-Key. Campaigns built in the app queue and pace themselves.
Carriers don't publish an official rate card, so every figure below is a round approximation:
- Fresh standard campaign, no brand vetting: about 1 to 4 segments per second.
- Higher use-case tiers: tens of segments per second, roughly 10 to 75.
Here is how long a send takes to leave the queue at those rates:
| Assumed rate | 10,000 segments | 30,000 segments |
|---|---|---|
| 1 segment/sec | ~2 hr 47 min | ~8 hr 20 min |
| 4 segments/sec | ~42 min | ~2 hr 5 min |
| 10 segments/sec | ~17 min | ~50 min |
| 75 segments/sec | ~2 min | ~7 min |
The table shows the first lesson of bulk sending. At low throughput, a 3-segment message takes three times as long to deliver as a 1-segment message. If a time-sensitive notice has to land before 11 PM, segment count and throughput together decide whether it does.
The T-Mobile daily cap
T-Mobile also caps messages per day per brand, based on your trust score. The approximate tiers, lowest trust score first, are ~2,000, ~10,000, ~40,000, ~100,000 and ~200,000 messages a day. At the lowest tier, a 10,000-person list with a few thousand T-Mobile subscribers can exhaust the day's allowance partway through the send. AT&T works differently. It throttles per second and per minute rather than with a hard daily cap.
Brand vetting ($79 Standard or $179 Enhanced, one-time) raises your trust score. It moves the daily cap only when the new score crosses a tier, and it does not change your per-second rate. Our post on whether brand vetting is worth $79 works through when the extra cap pays off. For most senders under a few thousand texts a day, standard registration is enough.
Quiet hours: why part of your send arrives later
Ready checks permitted local hours based on the recipient's area. A bulk API text, like any API or one-to-one text, is held only if the window opens within 6 hours; otherwise it is refused with 409 QUIET_HOURS_BLOCK and has to be sent again later. Campaigns, drip campaigns and automations built in the app wait up to 24 hours. If you chose Skip them for contacts in quiet hours, or the offer would expire before their window opens, those contacts are skipped and never get the text. A held text goes out when the window opens, and you are charged only when it actually sends.
For a national list, this changes the shape of the send. Fire a campaign at 9 AM Eastern and it's 6 AM in California. The East Coast portion goes out now, and the West Coast portion waits until local hours open. The opposite happens at night: a 7:30 PM Pacific send can push Eastern recipients past their window, and their morning is more than 6 hours away, so those texts are refused and have to be sent again once their window opens.
That is the intended behaviour, but plan for it:
- Don't promise "everyone gets this by X" unless your list sits in one time zone.
- Write timezone-neutral copy for national sends ("tonight 11 PM ET", not "tonight").
- Split by region if the timing matters. Send separate bulk requests per time-zone group, at a sensible local hour for each.
We wrote a longer piece on timezone-aware quiet hours for national sends, including how to sequence regional batches.
Opt-outs and refusals: what never sends and never bills
Three kinds of number in your to array are refused up front and not charged:
- Opted-out contacts. When someone replies STOP or UNSUBSCRIBE, Ready records the opt-out and it applies across campaigns. A list exported from your database last month can't re-text them by accident.
- Blocklisted numbers.
- Known landlines.
GET /v1/opt-outs lists the opted-out contacts on this account. It leaves out a STOP from a deleted contact, a dialer do-not-call entry and a STOP held on another account of the same family, so before a send from another tool, check the number with GET /v1/opt-outs/:phone. Our list will stop a send to an opted-out number, but your product should also stop trying. That keeps your own reporting honest and stops an "opted out" user from seeing a marketing toggle that's still on. Our guide to handling SMS opt-outs covers keyword variants and the common mistakes.
Every bulk message should still tell people how to stop. "Reply STOP to opt out" costs about 23 characters. In the next section you'll see why those characters can matter to the bill.
Segments drive the bill: a worked example
Carriers bill segments, not messages, and so do we. The rules:
- GSM-7 text (plain letters, numbers, common punctuation): 160 characters fit in one segment. Longer messages split into 153-character segments.
- Any emoji or other non-GSM character switches the whole message to unicode: 70 characters for one segment, 67 per segment once it splits.
- Ready swaps curly quotes, long dashes and ellipses for plain ones before sending, so text pasted from a doc doesn't silently trigger unicode.
Our rate is $0.0075/segment on Standard, plus a $0.0045 carrier fee per segment. Enterprise pricing is custom, so talk to us if that's you. The carrier fee is itemized on its own line. The worked example below assumes these numbers:
Version A: a 175-character promo with one emoji. Unicode, 175 ÷ 67 = 2.6, so 3 segments. Sent to 10,000 opted-in contacts: 30,000 segments. Rate: 30,000 × $0.0075 = $225, plus 30,000 × $0.0045 in carrier fees.
Version B: the same offer rewritten to 155 characters, no emoji. GSM-7, under 160, so 1 segment. Sent to the same 10,000 contacts: 10,000 segments. Rate: 10,000 × $0.0075 = $75, plus 10,000 × $0.0045 in carrier fees.
Dropping one emoji and cutting 20 characters saves $150 in rate, plus 20,000 segments' worth of carrier fees. It also cuts delivery time by two thirds at any throughput in the table above. These all-in figures (rate plus carrier fee) exclude the compliance surcharge. If you switch on scrubbing or landline validation for the account, each adds $0.003 per segment.
Two more costs to budget for:
- Replies are billed like sent texts: the per-segment rate plus the carrier fee. A bulk send to an engaged list generates replies. Engaged, opted-in lists reply a lot, so that line can be real money.
- Registration: $25 at submit, which is the first month. After that it's $25/mo for registration and $2/month per number. Most approvals take 2 to 7 days.
Billing is prepaid credits through Stripe, so top up before a big send. The pricing page shows the current per-segment card.
When a raw CPaaS is the better fit
We'll say this plainly. If you have engineers who will build opt-out tracking, quiet-hour holds, list scrubbing, registration flows and per-customer sub-accounts themselves, a raw CPaaS costs less per segment. Twilio lists about $0.0083/segment, well below our rate. At very high volume with an in-house compliance build, that gap adds up.
What our per-segment rate pays for is the layer above the carrier. Opt-outs are refused before they bill. Quiet hours are enforced per recipient area. Each customer gets a sub-account with its own number and 10DLC registration, and registration is self-serve from the app. You can also send to a validated, scrubbed list without writing your own scrub client. For a platform sending on behalf of many customers, that build is usually the expensive part, not the segment price.
The practical takeaway
A safe bulk send comes down to a short checklist:
- Only consented or replied contacts, with consent records kept in your own database.
- Normalize to E.164, dedupe, validate and scrub as the list's age and source require.
- Write the message to fit one GSM-7 segment where you can, and include opt-out language.
- Batch in code, with one stored
Idempotency-Keyper batch. - Expect pacing (seconds per segment, daily caps) and quiet-hour holds. Set timing promises around them.
If you want to try the request shapes above, create an account. The first 20 texts are free one-to-one sends, so use them to test before you register. A bulk send is paid from the first text, and you'll need a card on file to send.
FAQ
What is a bulk SMS API? A bulk SMS API is an endpoint that sends one message to many phone numbers in a single request. On Ready it's POST /v1/messages/bulk, which takes an array of numbers, the message body and a consent attestation. Campaigns in the app queue and pace delivery; the API refuses a batch that is over your throughput with a 429 and tells you how long to wait.
How fast can I send 10,000 text messages through an API? It depends on your carrier throughput, which ranges from roughly 1 to 4 segments per second for a fresh standard 10DLC campaign to tens per second on higher tiers. At an assumed 4 segments per second, 10,000 single-segment texts take about 42 minutes. Multi-segment messages and T-Mobile's daily brand cap can stretch that further.
Do I need 10DLC registration to send bulk SMS? Yes, for any real volume. Ready allows up to 500 texts a month before registration, and unregistered traffic gets filtered by carriers. Registration costs $25 at submit, and most approvals take 2 to 7 days.
How much does it cost to send 10,000 texts with Ready? It costs $0.0075 per segment plus a $0.0045 carrier fee per segment, so the segment count decides the total. Ten thousand 1-segment texts cost $75 in rate plus the carrier fees. The same list with a 3-segment emoji message costs $225 in rate plus three times the carrier fees, before any compliance surcharge you switch on.
What happens if someone on my bulk list has opted out? The text to that number is refused before it sends, and you aren't charged for it. Ready also refuses numbers on your Ban list and known landlines up front. GET /v1/opt-outs lists this account's opted-out contacts but not every blocked number, so check a number with GET /v1/opt-outs/:phone before another tool texts it.