#Opt-outs and scrubbing
Ready blocks opted-out numbers for you: it refuses sends to anyone in GET /v1/opt-outs, so you don't need to check before every send.
Details
Every opt-out (STOP, marked opted out, Do Not Disturb for texts) answers 403 opt_out_locked, with reason saying why (for example sms_stop_keyword or tcpa_scrub). A refused send isn't charged. It shows in GET /v1/messages as a failed row with cost_usd: 0.
How people opt out
An inbound STOP or similar keyword opts the contact out immediately. Texting START opts them back in, unless the lock came from TCPA scrubbing or your CRM.
Details
Keywords include STOP, STOPALL, UNSUBSCRIBE, CANCEL, END, QUIT, REVOKE, OPT OUT and similar, plus clear phrases like "stop texting me" and any custom keywords saved on your account earlier.
Only a keyword opt-out fires the contact_opted_out webhook (your own dnd_sms updates don't), so reconcile with GET /v1/opt-outs.
Opting someone out yourself
To record an opt-out from a call, an email or a web form, send PUT /contacts/:id with {"dnd_sms": 1}. This can't be undone over the API.
Details
Trying answers 403 opted_out, or 403 opt_out_locked if the contact also replied STOP. Only the contact texting START lifts it.
TCPA litigator scrubbing
Ready scrubs contacts automatically when they're created or imported and before sends. A match locks the contact like a STOP, and START doesn't lift it.
Details
The lock sets opt_out_locked=1, status=opted_out, opt_out_source="tcpa_scrub" and is_litigator=1, and records the matched lists in tcpa_status (comma-separated). Choose the lists with scrub categories.
Scrubbing costs your scrub rate per segment when it's on: $0.005 by default. Your rate is scrub_per_segment_usd in GET /compliance/scrub-categories.
Suppression fields on a contact
Every contact from /contacts carries these. Flags are integers, 0 or 1, not booleans.
Details
| Field | Meaning | Blocks texts? |
|---|---|---|
opt_out_locked | Set by a STOP or a TCPA lock. Cleared only by START. | Yes |
status | opted_out when opted out. | Yes, when opted_out |
dnd_sms | Do not text. | Yes |
dnd_email, dnd_calls | Do not email, do not call. | No |
opt_out_source | sms_stop_keyword, profanity_opt_out, tcpa_scrub, ghl_dnd, ai_intent, intent_scan, cross_family_stop_propagation, or null for a manual DND. | |
opt_out_at | When they opted out (ISO 8601). This is the real opt-out time. | |
is_litigator | Matched a TCPA litigator list. | Yes, through the lock |
tcpa_status | The matched lists, comma-separated, or null. | |
sms_invalid | The number can't receive texts. | Depends on an account setting |
Which list to use
Sync from GET /v1/opt-outs: it is exactly who Ready won't text. The CSV exports are for audits.
Details
| List | What's in it | Ready blocks texts to all of it? | Format |
|---|---|---|---|
GET /v1/opt-outs | Everyone Ready won't text: STOP, opted out, DND for texts, TCPA lock. | Yes | JSON, cursor pages |
?compliance=optout | The same people, minus contacts marked spam (unless include_spam=1). | Yes | CSV, 6 an hour |
?compliance=dnc | Do not contact on any channel (see compliance). | No: email-only and call-only DND contacts can still get texts | CSV, 6 an hour |
Keeping your own copy in sync
Walk GET /v1/opt-outs?limit=500 until has_more is false, listen for contact_opted_out, and walk the list again on a schedule for the other opt-outs. Keep your own copy: once a contact is deleted, the API reports its number as not opted out.
Details
Deleting a contact removes it from /v1/opt-outs. After a delete, a re-created contact and GET /v1/opt-outs/:phone show the number as not opted out. Ready keeps the opt-out in a separate record for its own send check, so sends to the number should still be refused, but don't rely on the API to tell you: check your copy before you add a number back.
#Check one number
/v1/opt-outs/:phoneWhether a number is opted out on your account.
Path parameters
phonestringRequired- US or Canada (+1) only, E.164 or 10 digits.
Details
URL-encode the+as%2Bif your client needs it. Other countries aren't supported: only the last 10 digits are used, so the answer is for a different number.
Returns
data.phoneE.164 string- The number checked.
data.opted_outbooleantrueif the number replied STOP, was marked opted out, or is Do Not Disturb for texts.data.sinceISO 8601 string | null- When the number opted out (the contact's
opt_out_at).nullwhen not opted out, or when Ready has no opt-out time.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | invalid_number | Fewer than 10 digits, or not a phone number. |
| 500 | internal_error | Check failed. Don't text the number; retry. |
#List opt-outs
/v1/opt-outsEvery number your account must not text, by contact, newest contact first.
Query parameters
limitintegerOptionalDefault100Max500- Page size.
before_idintegerOptional- The
next_before_idfrom the previous page.offsetisn't supported and is ignored.
Returns
data[]object[]phoneandsince(as in Check one number). Sorted by contact, not by opt-out date.has_moreboolean- Loop until it's
false. Don't loop onnext_before_id: it's set on the last page too. next_before_idinteger- Pass as
before_id.
Errors
| HTTP | Code | When |
|---|---|---|
| 500 | internal_error | Read failed. Don't treat the list as empty. |
#Scrub numbers
/v1/scrubChecks up to 1,000 US numbers against the TCPA litigator and do-not-call lists your account screens.
Details
It is the same rate as a send's scrub (scrub_per_segment_usd, $0.005 by default). A number your account scrubbed recently can cost less. Invalid numbers aren't charged.Headers
Idempotency-KeystringRecommended- Retry safely without paying twice.
Body parameters
phonesstring[]Required- 1 to 1,000 US numbers, E.164 or 10 digits.
Returns
data.totalinteger- Numbers in the request.
data.results[]object[]- One per number:
phone,normalized,valid,litigator,dnc_complainer,federal_dnc,state_dnc,blocked,cached,scrubbed_at.Details
A list flag ofnullmeans your account doesn't screen that list. On a problem,error_codeanderror:invalid_number(not charged),scrub_failed, orscrub_unavailable(not checked, not charged).
It creates no contacts. A number that's already one of your contacts gets the new result written onto it.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | no code | phones is missing, empty or not a list. |
| 400 | too_many_numbers | More than 1,000 numbers. |
| 402 | insufficient_balance | The batch could cost more than your balance (required_cents, balance_cents). |
| 403 | permission_denied | The key's user lacks edit_contacts. |
| 403 | test_key_not_allowed | Called with a test key. |
| 409 | scrub_off | Scrubbing is off. |
| 503 | tcpa_not_configured | Scrubbing isn't available right now. |
#Scrub one number
/v1/scrub/:phoneThe same check for one number. This GET is billed, like one number in Scrub numbers.
Path parameters
phonestringRequired- A US number, E.164 or 10 digits.
Returns
dataobject- One result, shaped like an item of
results[]above.
Errors
| HTTP | Code | When |
|---|---|---|
| 402 | insufficient_balance | Balance too low. |
| 403 | permission_denied | The key's user lacks edit_contacts. |
| 403 | test_key_not_allowed | Test keys can't make this billed read. |
| 409 | scrub_off | Scrubbing is off. |
| 503 | tcpa_not_configured | Scrubbing isn't available right now. |
#Get scrub categories
/compliance/scrub-categoriesWhich lists your account screens contacts against, and your scrub rate.
Returns
Fields are at the top level of data.
master_enforceboolean- Whether scrubbing is on at all.
categories[]object[]key(tcpa,dnc_complainers,federal_dnc,state_dnc),label,enabled,effective,default_on,source,attestation_required_to_disable.required_phrasestringI UNDERSTAND: whatconfirmation_textmust say to turn off a protected list.scrub_per_segment_usdnumber- Your scrub rate. The response has a few more billing fields used by the dashboard.
Errors
| HTTP | Code | When |
|---|---|---|
| 500 | internal_error | Read failed (the code is in error). |
#Change scrub categories
/compliance/scrub-categoriesTurns screening lists on or off. Account owner only.
error and the sentence in message (except invalid_field and OWNER_ONLY, below).Body parameters
categoriesobjectRequired- Any of
tcpaanddnc_complainers(on by default),federal_dncandstate_dnc(off by default), each a JSON boolean.Details
Send realtrueandfalse: onlytrue,1,"1"and"true"mean on, so"yes"or"on"silently mean off. Turning every list off turns scrubbing off. confirmation_textstringTo turn off tcpa or dnc_complainers- Exactly
I UNDERSTAND.
Returns
dataobjectmaster_enforceand the savedcategories.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | categories_required | categories is missing or not an object. |
| 400 | unknown_category | A key isn't one of the four. |
| 400 | attestation_required | Turning off tcpa or dnc_complainers without confirmation_text (disabling lists them). |
| 400 | invalid_field | confirmation_text is a list or object. Usual shape: code, error, field. |
| 403 | OWNER_ONLY | Caller isn't the account owner (code in code). |
| 403 | test_key_not_allowed | Called with a test key. |
#Export opt-outs as CSV
/contacts/exportA CSV of opted-out contacts, for audits and list hygiene.
Query parameters
complianceenumRequired- Must be exactly
optoutordnc. Any other value exports all your contacts.Details
optouteveryone Ready won't text: replied STOP, DND for texts, or locked by TCPA scrubbing.opt_out_sourcesays which. Fileopted-out-contacts.csv.dncdo not contact on any channel: the above plus email-only and call-only DND, litigators and invalid numbers. Not the federal Do-Not-Call registry. Filednc-list.csv.
include_spamintegerOptional- Contacts marked spam are left out unless you send
1. Add it for an audit-complete file.
The file starts with a UTF-8 byte-order mark (BOM): decode it as utf-8-sig in Python.
Details
Otherwise the first column name reads \ufeffid.
API keys can always export.
Errors
| HTTP | Code | When |
|---|---|---|
| 403 | EXPORT_DISABLED | A teammate, on an account where only the owner can export contacts. |
| 429 | rate_limited | More than 6 exports in an hour (in code). |