Message log and delivery
Read back how your texts did, and check a text before you send it.
#Export messages
Downloads your message log as a CSV file, newest first, including texts your team members sent. It takes the same filters as the Logs tab in Reports. It streams, so a day of a very large account downloads in one file. An export never comes back silently cut short: a file holds 500,000 messages for each day of the range (at least 5,000,000, at most 50,000,000); past that the file ends with a # Export stopped at line, and a read that fails part way ends it with # export aborted. Narrow the dates or filters and export again.
Parameters
fromdate, query- First day to include, as
YYYY-MM-DD, in your account’s time zone. todate, query- Last day to include, as
YYYY-MM-DD. The whole day is included. daysinteger, query- The last this many days, today included. Used when there’s no
from. With nofrom,toordays, the export covers all your messages. directionenum, queryinboundoroutbound.statusstring, query- Only messages with this status:
queued,delivered,failedorundelivered, the same words as the file’s status column.failedalso includes the undelivered ones.sentorpendingnarrowsqueuedto one of them. searchstring, query- Matches the contact’s name, the message text, or either phone number. A search with letters in it only looks through your newest 10,000 messages; a number search looks through all of them.
filtersJSON string, query- A list of conditions, each
{"field", "op", "value"}.fieldisstatus,dir,carrier,number,body,sentvia,channelorcampaign.opisis,is not,is any of,is none of,contains,does not contain,is emptyoris not empty. A condition Ready doesn’t know is skipped. Forsentvia,valueiscampaign,automation(drip campaigns and automations) ormanual. Any other value is skipped. matchenum, queryDefaultandorto match any condition infiltersinstead of all of them.sub_accountstring, queryallfor your main account and every sub-account, or one account’s id. Only the owner or an admin of the main account can use it. Adds thesub_account_idandsub_accountcolumns.
Returns
A CSV file (text/csv, named messages.csv) with a header row and one row per message. Columns:
idinteger- Ready’s numeric id for the message.
directionenuminboundoroutbound.from_numberstring- The number it came from.
to_numberstring- The number it went to.
bodystring- The exact text that went out, with any opt-out line or business name Ready added.
statusstringqueued,delivered,failed,undeliveredorunknown, as in the Message object. Other stored words, likereceived, appear as they are.segmentsinteger- Segments billed.
sent_atstring- When it was sent.
error_reasonstring- The stable cause of a failure, the same values as on the Message object. Empty when it didn’t fail.
error_detailstring- A readable explanation of the failure. Empty when it didn’t fail.
sub_account_idinteger- Only with
sub_account: the account the message belongs to. sub_accountstring- Only with
sub_account: that account’s name. contact_namestring- The contact’s first and last name. Empty when the number isn’t a saved contact.
campaignstring- The campaign or drip campaign that sent it. Empty for a one-to-one text or a reply.
media_urlstring- The link to the picture or file the message carried. Empty for a plain text.
message_idstring- The message’s public id, the
idon the Message object. channelenumsms,mmsorimessage, as the message log shows it.
Each account can run 6 exports an hour, shared with contact exports and the dashboard. A refused export doesn’t count.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | invalid_field | A filter was sent twice, like ?status=a&status=b, or sub_account isn’t all or an id. |
| 403 | permission_denied | Your role doesn’t have view_messages. |
| 403 | sub_account_scope_forbidden | Only the owner or an admin of the main account can use sub_account. Also organization_only. |
| 404 | sub_account_not_found | sub_account isn’t one of your accounts. |
| 422 | too_many_sub_accounts | Too many sub-accounts for all. Pick one. |
| 429 | rate_limited | Over the hourly export limit. Try again later in the hour. |
| 500 | no code | The export failed. Retry in a minute. |
| 503 | export_timezone_read_failed | Ready couldn’t read your time zone for a dated export. Retry in a minute. |
#Count failures by reason
Counts your failed outbound texts by cause, most common first. This is the "Why texts failed" card in Reports. Counts are exact, not a sample, and are cached for 5 minutes.
Parameters
fromdate, query- First day to include, as
YYYY-MM-DD, in your account’s time zone. todate, query- Last day to include, as
YYYY-MM-DD. daysinteger, query- The last this many days, today included. With no
from,toordays, the last 90 days. filtersJSON string, query- The same conditions as Export messages. A
statuscondition is ignored, since only failures are counted. matchenum, queryDefaultandorto match any condition infilters.sub_accountstring, queryallor one account’s id, as in Export messages.
Returns
data.by_reason[].error_reasonstring | null- The stable cause, the same values as
error_reasonon the Message object. Group and match on this.reasonholds the same value. data.by_reason[].error_detailstring | null- A readable explanation to show.
data.by_reason[].countinteger- Failed texts with this cause.
data.by_reason[].explanationobject | nulltitle,body,fix_action(likecheck_recipientorcontact_support) andseverity(likerecipientorcompliance).nullwhen Ready has no explanation for the cause.data.totalinteger- The sum of every
count.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | invalid_field | A filter was sent twice, or sub_account isn’t all or an id. |
| 403 | sub_account_scope_forbidden | Only the owner or an admin of the main account can use sub_account. Also organization_only. |
| 404 | sub_account_not_found | sub_account isn’t one of your accounts. |
| 422 | too_many_sub_accounts | Too many sub-accounts for all. Pick one. |
| 503 | reason_breakdown_unavailable | The counts couldn’t be loaded, often because the date range is too big. This never means zero failures. Retry, or pick fewer days. |
#Get your delivery rate
Your outbound delivery rate for the last 24 hours, 7 days and 30 days, with a count per carrier. Pass a window to get its rate too, and a second window to compare against. The rate counts only texts that reached a carrier and got an answer: delivered out of delivered plus failed. Texts Ready refused before sending are counted in blocked, not as failures. Home and Reports in the app show delivered out of every text sent, so this rate can read higher for the same days.
Parameters
daysinteger, query- A window of the last this many days, 1 to 365. Another value is ignored.
sinceISO 8601 string, query- Start of a window, like
2026-10-01or2026-10-01T00:00:00Z. A date alone is a whole day in your account’s time zone. Takes the place ofdays. A value in another format is ignored. untilISO 8601 string, query- End of the window. A date alone includes that whole day in your account’s time zone. A window can be up to 90 days. A lone
sinceoruntilis measured from now. carrierenum, query- Only texts to this carrier:
verizon,att,tmobile,uscellular,sprint,other,internationalorunknown. Applies to every rate. Another value is ignored. compare_to_daysinteger, query- A second window to compare with, like
days. Alsocompare_to_sinceandcompare_to_until, likesinceanduntil.
Returns
delivery_rate_24hobject- A rate for the last 24 hours, with the fields below.
delivery_rate_7danddelivery_rate_30dare the same for 7 and 30 days. delivery_rate_customobject | null- The rate for your window.
nullwithout one. delivery_rate_compareobject | null- The rate for the comparison window.
nullwithout one. delivery_rate_deltaobject | null- How your window (or the last 7 days, without one) moved against the comparison window:
delivered_pct_delta_pp(percentage points),total_volume_pct_change, andinterpretation:improving,degradingorflat(within 1 point).nullwithout a comparison window or when either has no answered texts. carrier_breakdown[]object[]- One row per carrier over your window, or the last 7 days without one:
carrier,total,delivered,failedandpending. The counts come back as strings of digits.carrieris one ofverizon,att,tmobile,uscellular,sprint,other,international, or(unknown)when no carrier is on record for the number.(unknown)rows cannot be filtered with?carrier=. filtersobject- The
carrierand window Ready applied, so you can see when a value was ignored. custom_window_labelstring | null- Your window in words, like
last 30d. Alsocompare_window_label.
Each rate object has:
deliveredinteger- Delivered texts.
failedinteger- Texts a carrier took and couldn’t deliver.
blockedinteger- Texts Ready refused before sending, such as opt-outs or landlines.
pendinginteger- Texts still waiting for a delivery report.
totalinteger- Every outbound text in the window.
delivered_pctnumber | null- Delivered out of delivered plus failed, one decimal.
nullwhen none were answered. Alsofailed_pct. pending_pctnumber- Pending out of total.
system_failedinteger- Failures on the carrier or network side.
sender_failedinteger- Failures the carrier put down to the message itself, like content filtering.
system_delivered_pctnumber | null- Delivered out of delivered plus
system_failed. notestring | null- A sentence when more than half the texts are still pending, so the rate covers only the rest.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | invalid_field | A parameter was sent twice. |
| 400 | invalid_date | since or until isn’t a real date, like 2026-02-30. |
| 400 | date_range_reversed | until is before since. |
| 400 | date_range_too_large | The window is over 90 days. span_days says how long it was. |
| 400 | compare_invalid_date | The same three checks on the comparison window: also compare_date_range_reversed and compare_date_range_too_large. |
| 500 | stats_unavailable | The rates couldn’t be loaded. Retry. |
| 503 | stats_too_slow | The window holds too many messages to count inside the request. Ask for fewer days. |
#Check a message for carrier blocks
Tells you if carriers or Ready’s content filter would block a text before you send it. It runs the same content check a real send gets: our keyword rules, then an AI review against US carrier policy. Nothing is sent and nothing is charged. You can run 30 checks an hour per account, shared by your team. Needs the send_sms permission.
Parameters
bodystringRequired- The message to check, up to 2000 characters. Merge fields like
{{contact.first_name}}are fine.
Returns
blockedbooleantruewhen this text would be blocked. At the top level.reasonstring | null- Why it would be blocked, in plain words.
nullwhen it passes. uncheckedbooleantruewhen the check didn’t finish. Withblockedfalse, the keyword rules passed but the AI review gave no answer, so the text was only partly checked. Withblockedtrue, the content review couldn’t run at all, so this says nothing about the text; a send right now would be held too. Either way, try again later for a full answer.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | invalid_field | body is an object or a list. |
| 400 | no code | body is blank or longer than 2000 characters. |
| 429 | no code | Your account ran 30 checks in the last hour. Wait and try again. |
| 500 | no code | The check didn’t run. Retry in a minute. |