readyDocs Open the appOpen the app

Opt-outs and scrubbing

Ready blocks opted-out numbers for you, so you don’t need to check before every send: a send to one is refused with a 403. The error_code is opt_out_locked for a STOP or an opted-out contact, dnd_sms for a contact marked Do Not Disturb for texts, and dnc_list for a number on your ban list. reason says why: opt_out_locked, status_opted_out, prior_opt_out, dnd_sms, dialer_dnc or tcpa_scrub. How the contact opted out (for example sms_stop_keyword) is the contact’s opt_out_source, not the reason. A refused send isn’t charged and shows in GET /v1/messages as a failed row with cost_usd: 0. Opt-out keywords and START are in Compliance, litigator scrubbing in DNC and scrubbing, and the suppression fields every contact carries are below.

Moving from another provider? Don’t text your old opt-out list one contact at a time. Import your contact file with Contacts → Import, mapping a column to Opted out. Those rows land opted out, and a later import never clears them. Steps: Switching from another tool.

Suppression fields on a contact

Flags are integers, 0 or 1, not booleans.

FieldMeaningBlocks texts?
opt_out_lockedSet by a STOP or a TCPA lock. A STOP lock is cleared only by the contact’s START; a TCPA lock is not cleared by START.Yes
statusopted_out when opted out.Yes, when opted_out
dnd_smsDo not text.Yes
dnd_email, dnd_callsDo not email, do not call.No
opt_out_source sms_stop_keyword (the person texted STOP), profanity_detected (a reply had profanity in it), tcpa_scrub (the number matched a TCPA list), ghl_dnd (marked Do Not Disturb in GoHighLevel), ghl_import (opted out in a GoHighLevel import; older contacts only), import (opted out in your import file), ai_intent (Ready read a reply as asking to stop), intent_scan and retroactive_intent_sweep (Ready reread older replies and found a stop request; the second is on older contacts only), cross_family_stop_propagation (the person texted STOP to another account in your agency), harassment_complaint and manual_abuse_threat (older contacts only, no longer written), marked_spam, assistant (opted out by Ask AI at your request), data_feedback_deceased (a reply said the person has died), or null for a manual DND.
opt_out_atWhen they opted out (ISO 8601). This is the real opt-out time.
is_litigatorMatched a TCPA litigator list.Yes, through the lock
tcpa_statusThe matched lists, comma-separated, or null.
sms_invalidThe number can’t receive texts.Depends on an account setting

Scrubbing starts off for new accounts. When it’s on in Settings, Ready scrubs contacts for TCPA litigators when they’re created or imported and before sends, at your scrub rate per segment when it’s on (your rate is scrub_per_segment_usd in Get scrub categories). A match sets opt_out_locked=1, status=opted_out, dnd_sms, dnd_email and dnd_calls to 1 and is_litigator=1, sets opt_out_source="tcpa_scrub" unless the contact already had a source of its own, and records the matched lists in tcpa_status. START doesn’t lift it.

To keep your own copy, walk GET /v1/opt-outs?limit=500 until has_more is false, listen for contact_opted_out (a STOP or other opt-out keyword fires it, and so does an opt-out Ready finds by reading a reply, or one you ask Ask AI to make; your own dnd_sms updates, TCPA scrubbing and CRM sync don’t), and walk the list again on a schedule. Deleting a contact removes it from List opt-outs, but Ready keeps its opt-out: Check one number still answers opted_out: true (with since: null) and sends to the number stay refused. Numbers on your ban list with no opted-out contact are not in the list either, unless you add scope=all. Check your copy before you add a number back.

#The Opt-out object

Check one number returns this object. List opt-outs returns phone and since for each opted-out contact.

phoneE.164 string
The number.
opted_outboolean
true if the number replied STOP, was marked opted out, is Do Not Disturb for texts, or is on your ban list (where a deleted contact’s opt-out is kept).
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: trying answers 403 with code CONTACT_OPTED_OUT (error_code opted_out), or CONTACT_OPT_OUT_LOCKED (error_code opt_out_locked) if the contact also replied STOP. A START from the contact lifts only their own STOP, never an opt-out you set, so treat yours as permanent.

sinceISO 8601 string | null
When the number opted out (the contact’s opt_out_at). null when not opted out, or when Ready has no opt-out time.

#Check one number

Whether a number is opted out on your account, including an opt-out on another account in your agency. Returns an Opt-out object in data.

Parameters

phonestring, pathRequired
US or Canada (+1) only, E.164 or 10 digits.
MoreURL-encode the + as %2B if your client needs it. Other countries aren’t supported: the check can match a US number with the same last 10 digits.
Errors
HTTPCodeWhen
400invalid_numberNot a phone number, or fewer than 10 digits. Send E.164, like +15551234567. Nothing is answered for a number this short.
500internal_errorCheck failed. Don’t text the number; retry.

#List opt-outs

Your account’s opted-out contacts, newest contact first. Narrower than Check one number: a deleted contact’s opt-out, your ban list and opt-outs on other accounts in your agency are not in it. Returns a list of Opt-out objects in data, each with phone and since, sorted by contact, not by opt-out date. Needs the view_contacts permission.

Parameters

limitinteger, queryDefault 100Max 500
Page size.
before_idinteger, query
The next_before_id from the previous page. offset isn’t supported and is ignored.
scopestring, query
all lists every number Ready won’t text on your account: your opted-out contacts first (source: "contact"), then your whole ban list (source: "ban_list"), which keeps a deleted contact’s STOP and every do not call number. A number on both is listed once for each. Opt-outs on other accounts in your agency still need Check one number. Page with cursor, not before_id.
cursorstring, query
With scope=all: the next_cursor from the previous page.
Returns
has_moreboolean
Loop until it’s false. Don’t loop on next_before_id: it’s set on the last page too.
next_before_idinteger | null
Pass as before_id. null when the page is empty.
next_cursorstring | null
With scope=all, in place of next_before_id: pass as cursor. null on the last page.
Errors
HTTPCodeWhen
400invalid_before_idbefore_id isn’t a whole number.
400invalid_scopescope is set to anything but all.
400invalid_cursorcursor isn’t a next_cursor this list gave.
500internal_errorRead failed. Don’t treat the list as empty.

#Export contacts as CSV

Every contact as a CSV, or with compliance only the opted-out ones, for audits and list hygiene.

Parameters

complianceenum, query
optout or dnc gives the opt-out file below. Any other value is refused with 400. Leave it out to get every contact instead, as contacts.csv with one column per custom field.
Possible values
  • optout your opted-out contacts: replied STOP, DND for texts, or locked by TCPA scrubbing. opt_out_source says which. Your ban list, a deleted contact’s opt-out and opt-outs on other accounts in your agency are not in it, though Ready won’t text them either; Check one number gives Ready’s full answer. Contacts marked spam are left out unless include_spam=1. File opted-out-contacts.csv.
  • dnc do not contact on any channel: the above plus email-only and call-only DND, litigators and invalid numbers, so not everyone in it is blocked from texts. Not the federal Do-Not-Call registry. File dnc-list.csv.
include_spaminteger, query
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, or the first column name reads id.

Errors
HTTPCodeWhen
403EXPORT_DISABLEDA teammate, on an account where only the owner can export contacts.
429rate_limitedMore than 6 exports in an hour per account, shared with the dashboard (in code). The RateLimit-Remaining header shows what’s left.

#Scrub numbers

Checks up to 1,000 US numbers against the TCPA litigator and DNC complainer lists, plus the federal and state Do Not Call lists if your account screens them. Each valid, unique number is billed at your scrub rate. Needs the edit_contacts permission.

The scrub rate is your account’s per-number rate (scrub_rate_usd in Get scrub categories), charged even when scrubbing on sends is off. On an account set to never re-scrub, a number it already paid to scrub is free. Otherwise every scrub is charged. Invalid numbers aren’t charged.

Parameters

Idempotency-Keystring, headerRecommended
Retry safely without paying twice (Idempotency).
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.
MoreLitigators and DNC complainers are always checked. federal_dnc or state_dnc is null when your account doesn’t screen that list. On a problem, error_code and error: invalid_number (not charged), scrub_failed, scrub_unavailable (not checked, not charged), or insufficient_balance (your balance ran out during the call, often from another call at the same time; 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
HTTPCodeWhen
400invalid_fieldphones is missing, empty or not a list (the code is in code and error_code).
400too_many_numbersMore than 1,000 numbers.
402insufficient_balanceThe batch could cost more than your balance (required_cents, balance_cents). Nothing was scrubbed or charged. A sub-account that pays with its own card and has not added one gets client_card_required instead: add a card in Billing.
503tcpa_not_configuredScrubbing isn’t available right now.

#Scrub one number

The same check for one number. This GET is billed at your scrub rate, like one number in Scrub numbers. Needs the edit_contacts permission.

Parameters

Idempotency-Keystring, headerRecommended
Retry safely without paying twice (Idempotency).
phonestring, pathRequired
A US number, E.164 or 10 digits.
Returns
dataobject
One result, shaped like an item of results[] in Scrub numbers.
Errors
HTTPCodeWhen
402insufficient_balanceBalance too low. A sub-account that pays with its own card and has not added one gets client_card_required instead: add a card in Billing.
403READ_ONLY_SESSIONYour access is view only. This lookup is charged, so it counts as a change.
503tcpa_not_configuredScrubbing isn’t available right now.

#Get scrub categories

Which lists your account screens contacts against, and your scrub rates. Fields are at the top level of data.

Only the account owner or an admin can change these lists, in the app under Settings → Compliance.

Returns
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. tcpa and dnc_complainers are on by default, federal_dnc and state_dnc off by default.
required_phrasestring
I UNDERSTAND: the phrase the owner must type to turn off tcpa or dnc_complainers.
scrub_rate_usdnumber
Your per-number rate for Scrub numbers. 0 on a free account.
scrub_per_segment_usdnumber
What scrubbing adds per segment on sends. 0 when scrubbing is off. The response has a few more fields used by the dashboard (can_change and billing rates).
Errors
HTTPCodeWhen
500internal_errorRead failed (the code is in error).