An SMS opt out API records every STOP reply your numbers receive and refuses later texts to those numbers, so no send path in your product can message someone who opted out. On Ready, inbound STOP, UNSUBSCRIBE and the other opt-out words are honored automatically, texts to opted-out numbers are refused before sending and not charged, and you read the list with GET /v1/opt-outs and GET /v1/opt-outs/:phone to keep your own database in sync.
The hard part of opt-outs isn't parsing the word STOP. It's making sure every path that can send a text checks the same list: your bulk campaign job, transactional notifications, the support agent typing in an inbox, a drip campaign, a cron job someone wrote in 2023 and forgot about, and now an AI agent with a send tool. One missed path is one text to someone who told you to stop. Under the TCPA, each of those texts can cost $500 to $1,500. This post covers the carrier rules, what Ready enforces at the send layer, and how to mirror the list into your own system so the rest of your product knows.
What carriers and regulators expect when someone texts STOP
You don't need to memorize the rulebook, but you need the shape of it, because your architecture has to satisfy it.
The keyword baseline
Carrier guidance (CTIA's messaging principles, which the carriers enforce through 10DLC) expects every business sender to honor the standard opt-out keywords: STOP, UNSUBSCRIBE, CANCEL, END, QUIT and STOPALL are the common set. After an opt-out, you send one confirmation that says the person won't get more messages, and then nothing else from that program. Registration reviewers check for this too. Your campaign's sample messages and opt-in flow are expected to mention how to opt out, which is why "Reply STOP to opt out" shows up in so many first messages.
Revocation by "any reasonable means"
The FCC has also made it clear that consumers can revoke consent in any reasonable way, not just with a magic keyword, and that senders have to honor it quickly (the current rule gives you no more than 10 business days). "Please stop texting me," "remove me," or a support email asking you to stop all count. Ready's handler catches the common keywords and plain phrases sent as a text reply. Requests that arrive another way (email, phone, a support ticket) are yours to record. We wrote about the messy variants in someone replies 'Cancel' instead of 'STOP' and keeps getting texts.
What this means for your system
- One opt-out has to suppress the contact across every campaign and send path, not just the campaign they replied to.
- The suppression has to hold even when the send comes from code that never looked at your contacts table.
- You need a record of when the opt-out happened, in case it's ever disputed.
How the SMS opt out API works on Ready
Here's what happens without you writing any code.
Inbound STOP is captured automatically
When a contact replies STOP, UNSUBSCRIBE or another opt-out word or phrase to one of your numbers, Ready records the opt-out and propagates it, so that contact can't be messaged again across campaigns on that account. You don't run a keyword parser or write a webhook handler to get this behavior. The reply also lands in the Inbox like any other inbound text, and it's billed like any inbound reply (the $0.0075/segment rate plus a $0.0045 carrier fee).
Texts to opted-out numbers are refused before sending
This is the part that matters for developers. When any request tries to text an opted-out number, Ready refuses it up front. It never reaches the carrier and you're not charged for it. The same goes for numbers on your Ban list, and for known landlines.
That refusal happens at the API layer, not in a UI checkbox. It applies to:
POST /v1/messages(single sends)POST /v1/messages/bulk(the same body to many numbers)- campaigns and drip campaigns built in the app
- the inbox, and GoHighLevel sends for connected accounts
- sends made through our MCP server by an AI agent
So if a teammate writes a new notification job next quarter and forgets to check your suppression table, the text still doesn't go out. That backstop is the main reason not to build opt-out enforcement yourself: your version is only as good as the discipline of every engineer who touches a send path.
Worked example: what refusal saves
Assumptions for this example: a 10,000-number bulk send, a one-segment message (under 160 plain characters), and 3% of the list opted out at some point since you built it. The 3% is a round assumption, not a benchmark.
- Opted-out numbers in the request: 10,000 × 3% = 300
- Texts refused and not charged: 300
- Texts sent: 9,700, billed at 9,700 × ($0.0075 + $0.0045)
- Message charges avoided on the refused 300: 300 × $0.0075 = $2, plus 300 × $0.0045 in carrier fees
Ready's all-in prices per segment include the carrier fee but not screening: Scrub lists and Number validation, the two switches in Settings under Compliance, each add $0.003 per segment when switched on, shown as screening on your bill, and both are off by default. Unless you turn the opt-out footer off, the first text to each contact, and one again from time to time, also gets "Reply STOP to opt out" and your business name if the message does not already have them, so a text close to the segment limit can bill one more segment.
The saving on the rate is small. What matters is the 300 texts that would have gone to people who said stop. At the TCPA's $500 to $1,500 per text, that's $150,000 to $450,000 of theoretical exposure from one careless send. The refusal is risk reduction, not immunity. Compliance stays the sender's responsibility, and refusal only covers opt-outs Ready knows about.
Reading the opt-out list
Two read endpoints cover almost every need. Both use the same base URL and X-API-Key header as the rest of the API.
Pull the full list
curl https://api.tryready.com/v1/opt-outs \
-H "X-API-Key: $READY_API_KEY" Use this for reconciliation: a scheduled job that pulls the list and makes sure every number on it is marked suppressed in your database. If your list is long, page through it the way the opt-outs API docs describe. Check the docs for the exact response shape rather than guessing at field names.
Check one number
curl https://api.tryready.com/v1/opt-outs/+12125550199 \
-H "X-API-Key: $READY_API_KEY" Use this when you're about to do something a send-time refusal can't cover: re-adding a contact to a campaign, showing an "opted out" badge on a profile, deciding whether a sales rep should call, or answering a support ticket that asks "why didn't this customer get our text?"
Sending stays the same
You don't change your send code to respect opt-outs. A normal single send looks like this:
curl -X POST https://api.tryready.com/v1/messages \
-H "X-API-Key: $READY_API_KEY" \
-H "Idempotency-Key: 5f1c2a8e-7b3d-4e9a-9c61-2d0f8b4a1e77" \
-H "Content-Type: application/json" \
-d '{"to": "+12125550199", "body": "Your order #1042 has shipped.", "consent_attested": true}' If that number opted out, the request is refused and nothing is billed. consent_attested is your promise that the recipient agreed to texts. Ready records that attestation per send (who sent, to how many, when) as an audit trail, but it's not a per-contact consent record. The opt-in time, wording and page URL for each contact have to live in your own database. Keep the Idempotency-Key on every send so a retried request never texts twice. That's its own failure mode, covered in how to stop duplicate texts from your SMS API.
Syncing opt-outs to your own database
Ready's refusal protects the SMS channel. Your database still needs to know, for three reasons: your UI should show it, other channels (calls, email) may need to respect it, and you want a list that survives if you ever change providers.
A sync design that holds up
- Keep a suppression table you own. Store the number in E.164, the source (
sms_stop,support_request,email_replyor whatever applies), and the time you learned about it. - Reconcile on a schedule. Every hour or every night, pull
GET /v1/opt-outsand upsert every number into your table. That's idempotent and cheap. The list holds only opted-out contacts on this account: a STOP from a deleted contact, a dialer do-not-call entry and a STOP held on another account of the same family are left out, so checkGET /v1/opt-outs/:phonebefore any send from another tool. - Check before non-SMS actions. Before a rep calls or a contact gets re-enrolled, check your table. Use
GET /v1/opt-outs/:phoneif you need the live answer. - Handle opt-outs that don't arrive as STOP. A support ticket that says "stop texting me" never touches the keyword handler. The read endpoints listed here don't write to Ready's list, so record these in your own table and filter them out before you call the send endpoint. Check the docs for anything beyond the read endpoints.
- Never delete rows. Re-subscribing is a new opt-in with its own consent record, not removing the old opt-out.
Which layer protects which send path
| Send path | Ready refuses opted-out numbers | Your suppression table needed for |
|---|---|---|
POST /v1/messages from your backend | Yes, uncharged | Showing status in your UI |
POST /v1/messages/bulk | Yes, uncharged | Cleaner lists and accurate send counts |
| App campaigns and drip campaigns | Yes | Reporting |
| GoHighLevel sends (connected accounts) | Yes | GHL contact fields, if you mirror them |
| Calls from the Ready Dialer | Yes, refused | Calls from a dialer outside Ready |
| No | Your own cross-channel policy | |
| Opt-outs by email, phone or ticket | Not unless recorded | Filtering before you send |
Whether a text opt-out should also stop calls is a policy decision. We cover the tradeoffs in why opt-out rarely crosses channels, and the fix.
If you run sub-accounts
Platforms that give each customer a sub-account face one more decision. If a consumer texts STOP to your customer A's number, should your customer B, who also has that consumer as a contact, be blocked too? Usually not, because consent belongs to the relationship with each business. Make that rule explicit in your schema (key the suppression on sub-account plus phone), and don't let it be decided by accident. On Ready, a STOP to any sub-account blocks that number for the parent account and every sub-account; check GET /v1/opt-outs/:phone for Ready's answer.
Where building it yourself is fine
To be fair to the alternatives: if you're on a raw CPaaS like Twilio or Telnyx, they have opt-out tooling too, and a competent team can build keyword handling, a suppression table and a send-side check in a few days. Those providers cost less per segment than Ready does. If you have one send path, one engineer who owns it and no plans to add more, building it yourself is reasonable.
The case for letting the platform enforce it gets stronger as send paths multiply: campaigns plus transactional plus inbox plus GHL plus AI agents, across many sub-accounts. Each new path is another place a check can be forgotten. A refusal at the send layer doesn't depend on anyone remembering.
There's one specific trap if you're moving providers. Your STOP history doesn't port with your numbers. Export it and suppress it on day one, as described in port your number from Twilio and your STOP list doesn't come with it.
Putting it together
The practical setup is short:
- Let Ready catch STOP, UNSUBSCRIBE and the other opt-out replies and refuse later sends to those numbers. That's automatic and uncharged.
- Run a scheduled
GET /v1/opt-outsreconcile into a suppression table you own (it lists this account's opted-out contacts only). - Use
GET /v1/opt-outs/:phonebefore calls, re-enrollments and support answers. - Record opt-outs that arrive by other means in your own table and filter before you send.
- Keep per-contact consent records yourself.
consent_attestedis an attestation, not proof of opt-in.
If you want to test this, you can sign up, add a card, and use the first 20 free one-to-one texts to send to your own phone, reply STOP, and watch the next send get refused. Do that before you register, because the free texts end once a registration is paid for. Pricing is on the pricing page.
FAQ
What is an SMS opt-out API?
An SMS opt-out API stores the numbers that have replied STOP and lets your code read or enforce that list. On Ready, opted-out numbers are refused at send time automatically, and you read the list with GET /v1/opt-outs or check one number with GET /v1/opt-outs/:phone.
Do I get charged for texts sent to people who opted out?
No, Ready refuses texts to opted-out numbers before they're sent and doesn't charge for them. The same applies to numbers on your Ban list, and to known landlines.
Do I have to build my own STOP keyword handler?
Not for STOP replies. Ready honors STOP and the other standard opt-out words, and plain-language replies such as "please stop texting me" or "remove me", and suppresses the contact across campaigns. You still need to record requests that never arrive as a text reply, such as a support email or a phone call, since regulators expect you to honor revocation made by any reasonable means.
How do I keep my database in sync with opt-outs?
Run a scheduled job that pulls GET /v1/opt-outs and upserts every number into a suppression table you own. That list holds only opted-out contacts on this account (a STOP from a deleted contact, a dialer do-not-call entry and a family-account STOP are left out), so check GET /v1/opt-outs/:phone before a send from another tool or a provider migration. Add opt-outs that arrive by email or phone to that table yourself, and filter them out before calling the send endpoint.
Does an SMS opt-out also stop phone calls?
A STOP also blocks calls placed from the Ready Dialer, which refuses a number on the opt-out record, but it does not reach email or a dialer outside Ready. If your policy is to stop all contact, check your own suppression table before a rep dials from another tool, and treat that as a decision you make deliberately.