Most contact lists start in a spreadsheet. Someone exported a CRM, a form tool or a point-of-sale system, and now there's a file with a phone column that looks like (212) 555-0199, 2125550199, 1.21256E+10 and 212.555.0199 ext 4. All of those can sit in the same column.
You can send to that file, but only after you fix it. A messy column costs you money on numbers that can't receive texts. A list with no consent record costs you more. This guide covers how to format the numbers, how to clean and consent-check the list, and how to send it, either by importing it into Ready or by pushing it through our API in batches.
Step 1: Stop Excel from damaging your phone numbers
Excel treats a phone number as a number, which causes three problems:
- Scientific notation. Long digit strings turn into
1.21256E+10. If you save the file in that state, the real digits are gone for good. - Stripped plus signs. If you type
+12125550199, Excel may store it as12125550199. - Silent rounding. Past 15 digits, Excel rounds. That rarely affects US numbers, but it does affect messy cells that have extensions stuck to the end.
The fix is to format the phone column as Text before you paste or import anything (Format Cells → Text). If the file already exists as a CSV, don't double-click it. Use Data → From Text/CSV and set the phone column to Text in the import dialog.
Step 2: Normalize every number to E.164
Carriers and APIs expect E.164: a plus sign, the country code, then the number, with no spaces. A US number looks like +12125550199. On Ready, a bare 10-digit US number is treated as +1. Normalizing the whole column anyway makes deduping reliable.
Put a helper column next to your raw numbers. This formula strips the usual punctuation from A2:
=SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(A2,"(",""),")",""),"-","")," ",""),".","") In the next column, add the country code based on length:
=IF(LEN(B2)=10,"+1"&B2,IF(AND(LEN(B2)=11,LEFT(B2,1)="1"),"+"&B2,"CHECK")) Filter the result for CHECK. Those rows have extensions, missing digits or international numbers, and a person needs to look at them. Don't guess. A wrong digit sends your message to a stranger who never opted in.
Dedupe after normalizing, not before
(212) 555-0199 and 2125550199 look like two different contacts until both become +12125550199. Run Data → Remove Duplicates on the normalized column. If you skip this, the same person gets your message twice, and you pay for both.
Step 3: Consent-check the list before anything else
This step decides whether the list is sendable at all. Formatting is mechanical. Consent is not.
Texting someone who didn't agree to it exposes you under the TCPA, at roughly $500 to $1,500 per text in statutory damages. Ask where every row came from:
| Source of the row | Sendable by SMS? |
|---|---|
| Checkout or form opt-in with SMS disclosure | Yes |
| Keyword join (they texted you first) | Yes |
| Existing customer who replied to your texts | Usually, within what they agreed to |
| Purchased or scraped list | No: use phone or mail |
| "They gave us their number once" with no record | Treat as no until confirmed |
Our position is simple: cold contact goes by phone and mail, and SMS goes to people who consented or replied. If a chunk of your sheet came from a list broker, delete those rows from the SMS file.
Keep the consent columns in your file
Add three columns: opt-in source, opt-in date and opt-in wording or page URL. This matters because Ready records the sender's attestation for each bulk send and each API send that sets consent_attested (who sent it, to how many, and when). That gives you an audit trail of your sends. It is not a per-contact consent record. Ready does not store each recipient's opt-in time, wording or page, so your spreadsheet or the system it came from has to keep that information. If you're starting a list from scratch, our guide on how to build an SMS marketing list covers collecting consent properly from day one. (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.)
Optional: validate and scrub
Two checks are worth running on older lists:
- Number validation (
GET /v1/validate/:phone, see docs.tryready.com) flags numbers that can't take texts, at $0.003 for each number that gets an answer. Landline validation during sends is off by default. When you switch it on for an account, it costs $0.003 extra per segment. - TCPA and DNC litigator scrub checks numbers against known litigator and DNC-complainer lists. The standalone scrub (
GET /v1/scrub/:phone) is $0.005 per contact. Scrubbing during sends is a separate setting, also off by default, at $0.003 extra per segment when on.
These checks reduce risk. They don't make you lawsuit-proof, and compliance stays your responsibility as the sender.
Step 4: Write the message and count segments
Every segment is billed, so check the length of your copy before you send it to thousands of people.
- Plain text (GSM-7): 160 characters for one segment, 153 per segment once a message splits.
- One emoji or other non-GSM character: 70 characters for one segment, 67 per segment after a split.
Copy pasted from Word or Excel often brings curly quotes and long dashes with it. On other platforms those can push a message into unicode. Ready swaps them for plain characters before sending, so they don't change your count with us. Emojis still do.
Worked example: merge fields and an emoji
Assumptions: 2,000 consented contacts, and a template with a {first_name} merge field. With short names, it runs about 150 characters.
- Plain text, 150 characters: 1 segment. Cost is 2,000 × $0.0075 = $15 in Standard rate, plus 2,000 × $0.0045 in carrier fees.
- Someone named "Christopher" adds 6 characters, which pushes their message to 156. That's still 1 segment, but a few longer names over 160 become 2.
- Add one 🎉 to the 150-character version and every message becomes 3 segments (150 ÷ 67, rounded up). Cost is 2,000 × 3 × $0.0075 = $45 in rate, plus 6,000 × $0.0045.
That emoji tripled the bill. These figures exclude the compliance surcharge if you've switched on scrubbing or landline validation. For a fuller breakdown, see bulk SMS cost per message.
Step 5: Register before you send at volume
US carriers filter unregistered 10DLC traffic. On Ready, registration happens in-app. You pay $25 at submit (the first month), and the ongoing monthly cost is $25/mo for registration + $2 per number. Most approvals take 2 to 7 days. An account can send up to 500 texts a month before it registers. If you bought a number before registering, you send on Ready's shared registration at the same per-text rate while yours is in review. Our 10DLC explainer covers what the carriers are looking for.
The first 20 texts on a new account are free one-to-one sends. Use them to test your formatting before you register, because they end once a registration is paid for. Campaigns are billed from the first text, and to send any text you need a card and a number ($2 a month, the first month charged when you get it).
Throughput sets your timeline
A fresh standard campaign usually gets about 1 to 4 segments per second. At an assumed 2 segments per second, 6,000 segments take 3,000 seconds, or about 50 minutes. T-Mobile also caps daily volume by brand trust score. The lowest tier is around 2,000 messages a day, so a large list may need to go out over more than one day.
Step 6a: Send by importing the CSV
Save the cleaned file as CSV UTF-8 so accented names survive. Keep only the columns you need: normalized phone, first name, and your consent columns for your own records. Then import it into Ready's contacts, build a bulk campaign from a template and schedule it.
A few things happen during the send:
- Opt-outs and numbers on your Ban list are refused up front and not charged.
- Quiet hours hold texts that would land outside permitted local hours for the recipient's area. Held texts go out later and are charged only when they send. 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 and must be resent; a one-to-one reply to someone who texted you in the last 24 hours goes out at any hour.
- STOP replies are honored, and the opt-out applies to every future campaign.
Replies land in the Inbox, where each one is billed like a sent text. For planning the campaign itself, see how to run an SMS campaign.
Step 6b: Send the CSV through the API
If your list lives in a pipeline instead of a person's laptop, POST /v1/messages/bulk sends one body to up to 100 recipients per call. Split larger files into batches:
import csv, time, uuid, requests
rows = [r["phone"] for r in csv.DictReader(open("clean.csv", encoding="utf-8"))]
body = "Your spring appointment slots are open. Reply STOP to opt out."
for i in range(0, len(rows), 100):
batch = rows[i:i+100]
# The key comes from the batch, so rerunning the script never sends a batch twice
key = str(uuid.uuid5(uuid.NAMESPACE_URL, f"clean.csv:{i}"))
while True:
r = requests.post(
"https://api.tryready.com/v1/messages/bulk",
headers={"X-API-Key": "YOUR_KEY", "Idempotency-Key": key},
json={"to": batch, "body": body, "consent_attested": True},
)
if r.status_code == 429: # faster than your carrier cap; nothing was sent
time.sleep(r.json().get("retry_after_ms", 1000) / 1000)
continue
r.raise_for_status()
break
d = r.json()["data"]
print(i, "sent", d["sent"], "held", d["held"], "failed", d["failed"]) consent_attested: true is your promise that each recipient agreed to texts. Only send it for rows that pass Step 3. The Idempotency-Key makes a retried request safe, so a network blip never sends a batch twice. Check data.failed and the per-recipient results in each response; a 429 means nothing in that batch was sent, so wait and send it again with the same key.
The bulk endpoint sends one identical body to everyone in the batch. If you need per-person merge fields, loop over rows and call POST /v1/messages once per contact. Full details are in the bulk messages docs and the quickstart.
The short version
- Format the phone column as Text so Excel can't change the digits.
- Normalize to E.164, then dedupe.
- Remove every row without a consent source, and keep consent columns in your file.
- Count segments, and think hard before adding an emoji.
- Register, test with the free texts first, and plan around throughput.
- Import into a bulk campaign, or batch 100 at a time through the API.
If you have a cleaned file ready, create an account and send a few one-to-one tests to your own team's phones first. That tells you whether the formatting held up before the whole list goes out.