ready. Try Ready →Sign up

#Quickstart: send your first text in 5 minutes

You need a Ready account with a card on file and a phone that can receive texts. Trial texts go out from a shared Ready number.

Node examples use built-in fetch and top-level await (an .mjs file, Node 20+). Python examples use requests.

  1. Get an API key 1 min

    Open Settings → Integrations, click Generate new key and copy it (shown once, starts with rsms_).

    Important. Generating a key replaces your live key, and the old one stops working immediately. An account has one live key; a test key can exist alongside it.
    Shell
    export READY_API_KEY="rsms_your_key_here"
  2. Check the key works 30 s

    401 AUTH_REQUIRED: the variable is usually empty. 401 INVALID_API_KEY: the key was revoked, mistyped or cut off.

    GET /v1/account
    curl "https://api.tryready.com/v1/account" \
      -H "X-API-Key: $READY_API_KEY"
    Response
    {
      "success": true,
      "data": {
        "balance_usd": 25,
        "balance_cents": 2500,
        "plan": null,
        "sms_rate_usd_per_segment": 0.05,
        "carrier_fee_usd_per_segment": 0.0045
      }
    }
  3. Send a text to your own phone 1 min

    Put your own mobile number in to. It arrives in 5 to 30 seconds.

    POST /v1/messages
    curl -X POST "https://api.tryready.com/v1/messages" \
      -H "X-API-Key: $READY_API_KEY" \
      -H "Idempotency-Key: $(uuidgen)" \
      -H "Content-Type: application/json" \
      -d '{
        "to": "+12125550199",
        "body": "Hello from Ready",
        "consent_attested": true
      }'
    Response
    {
      "success": true,
      "data": {
        "message_id": "179088672338905475132359",
        "segments": 1,
        "sent_body": "Hello from Ready Reply STOP to opt out.",
        "compliance_text_added": "Reply STOP to opt out.",
        "status": "queued",
        "sent_at": "2026-10-01T20:32:03.473Z",
        "sent_at_unix": 1790886723,
        "credits_remaining": 499,
        "carrier_fee": 0.0045,
        "balance_usd": 24.95,
        "carrier_balance": 24.9455
      }
    }
  4. Read the response 30 s

    successboolean
    true: Ready accepted the text.
    data.message_idstring
    Store it: delivery lookups and webhooks use it.
    data.statusstring
    queued, then delivered or failed seconds later.
    data.segmentsinteger
    What you're billed for. A plain segment is up to 160 characters.
    data.sent_bodystring
    The exact text sent. Ready may add your opt-out line and business name to meet carrier rules; sent_body is the exact text sent and segments are counted on it. Include your own opt-out in the body to stop us adding ours.
    Tip. 200 means accepted, not delivered. For delivery, use a webhook (next step) or Get a message.
  5. Get delivery updates and replies 2 min

    Point Ready at a public HTTPS URL that accepts POST (a free request inspector works). Send "regenerate_secret": true the first time to get the signing secret in the response.

    Reply from your phone and an inbound_reply event arrives. Before going live, verify the signature on every event.

    PUT /v1/webhooks
    curl -X PUT "https://api.tryready.com/v1/webhooks" \
      -H "X-API-Key: $READY_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "url": "https://you.example.com/ready-webhook",
        "events": [
          "message_delivered",
          "message_failed",
          "inbound_reply"
        ],
        "regenerate_secret": true
      }'
  6. Go live checklist

Before your first send

  • Trial: sends go out from a shared Ready number and ignore from. A card is required (402 CARD_REQUIRED). A small allowance of free sends, which are real texts, ends in 402 SANDBOX_LIMIT.
  • After campaign approval: you send from your own numbers and pay from your balance (402 insufficient_balance). No active number: 400 no_sender_number.

Stuck? Email support@tryready.com with the response's X-Request-Id.

#Authentication

Server-to-server calls use an API key. Third-party apps such as Zapier use OAuth 2.0.

API keys

Send the key as X-API-Key: rsms_… or Authorization: Bearer rsms_… (capital B). If both are present, the bearer token is used.

  • Keys start with rsms_ and are shown once. Only the account owner can create one, and it acts with the owner's full permissions.
  • One live key per account. Generating a new one replaces it, and the old key stops working immediately.
Walkthrough: create an API key3 steps
  1. Open Settings (the gear, top right), then Integrations.
  2. In the API section, click Generate new key. If you already have a key, this replaces it: the old key stops working immediately. Update every system that uses it first.
  3. Copy the key. It's shown once and starts with rsms_.

Test keys

A key starting rsms_test_ simulates sends, purchases and registration: nothing is sent or charged. Other writes are refused and reads are real. It reads your real account, so keep it as safe as a live key.

Walkthrough: create a test key3 steps
  1. Open Settings (the gear, top right), then Integrations.
  2. In the API access section, choose Test key next to Generate new key, then click it. If you already have a test key, this replaces it, as does Replace on its row: the old test key stops working immediately. Revoke on the row turns it off.
  3. Copy the key. It's shown once and starts with rsms_test_.

An account has one test key. Only the account owner can create, replace or revoke it, the same as a live key, and making or revoking a test key never touches your live key.

Details: what a test key checks and returns

A test key checks the request itself (required fields, types, lengths and Ready's global block list) and that from is one of your numbers (invalid_from), but not the rest of your account's state. Sends aren't checked against opt-outs, DND, your balance, consent_attested or the 60-second duplicate guard (duplicate_send), and to isn't checked for being a real phone number ("123" is accepted). Number purchases don't check payment_intent_id: a made-up id, a reused one or none at all succeeds. Registration calls don't check that the id is yours or that the payment exists, and skip the EIN-to-name match (ein_name_mismatch), duplicate, brand-first (brand_required_first) and vetting-state checks. The format checks (invalid_ein, address_incomplete, business_description_required, consent_required) still run. A 200 from a test key doesn't mean the live call will succeed.

  • Simulated: sending texts (POST /v1/messages, /v1/messages/bulk, /sms/send and the Zapier Send SMS action), number checkout and purchase, every registration write, POST /v1/validate, and creating client accounts and setting their markup.
  • Refused: every other write (POST, PUT, PATCH, DELETE) answers 403 with { "success": false, "error": "test_key_not_allowed", "message": "Test keys can only call simulated endpoints. Use a live key for this." } and changes nothing. That covers contacts, conversations, webhook settings, screening lists (/compliance/scrub-categories), POST /v1/scrub, /sms/generate and number releases (DELETE /v1/numbers/:id). Two billed reads are refused the same way: GET /v1/scrub/:phone and GET /ghl-dialer/token. The code is in error, not error_code.
  • Reads are real: every other GET works and reads your real balance, rates, numbers, messages and contacts.
  • Simulated responses add "test": true and a note, and some shapes differ. POST /v1/messages and POST /sms/send return the live data object, with the same fields repeated at the top level: { "success": true, "test": true, "data": { "message_id": "test-msg-…", "segments": 1, "status": "accepted", "sent_body": "…", "compliance_text_added": null }, "message_id": "test-msg-…", "segments": 1, "status": "accepted", "sent_body": "…", "compliance_text_added": null, "note": "test mode: no real message sent, no charge" }. sent_body and compliance_text_added preview the opt-out line and business name a live send would add, and segments is counted on sent_body. Both fields are left out if your settings can't be read. The message isn't stored, so GET /v1/messages/:id answers 404 message_not_found for it. POST /v1/messages/bulk gives each result a test-msg-… message_id and its segments, but still checks your balance first, so a low balance can get 402 insufficient_credits. POST /v1/numbers returns data.phone_number with no phone_number_id. Registering a brand or campaign returns registration_id: null. Registration resubmit, vet, vet/confirm and cancel return data.reg_id with test and note, not the live shape; rename returns only data.reg_id and test. Confirm returns no registration_id. Creating a client account returns id: null. Checkout returns a test-… payment_intent_id and no client_secret. Validate returns line_type: "unknown", carrier: null and textable: null. Setting a client's markup returns only data.id and test.
  • No delivery webhooks: test sends never trigger message_delivered or message_failed. To try your endpoint, use Send test (see Webhooks).
  • MCP: on the MCP server a test key works as a bearer token, but only send_sms is simulated. Every tool that changes or bills something, including send_campaign, enroll_in_drip and ai_generate_message, is refused with test_key_not_allowed, so it sends and charges nothing. Tools that only read work.

OAuth 2.0

The authorization-code flow of the published Zapier app. Tokens from /zapier/oauth/token are JWTs: send Authorization: Bearer <token>. Errors follow RFC 6749 (error, error_description). The MCP server has its own OAuth flow.

Acting as a client account

Agencies send X-Sub-Account-Id with an API key to run a /v1 call as a client account. See Client accounts.

Authentication errors

In code: 401 AUTH_REQUIRED (no key, empty variable, or lower case bearer), 401 INVALID_API_KEY, 401 INVALID_TOKEN, 403 ACCOUNT_SUSPENDED, 503 AUTH_CHECK_UNAVAILABLE (nothing ran; retry).

Header, option 1
X-API-Key: rsms_your_key_here
Header, option 2
Authorization: Bearer rsms_your_key_here
Response
{
  "success": false,
  "error": "Sign in to continue. For API calls, send your API key in the X-API-Key header.",
  "code": "AUTH_REQUIRED"
}

#Errors

Errors have a non-2xx status (three exceptions below) and a JSON body:

successboolean
Always false.
errorstring
A sentence for people. It can change: don't parse it.
DetailsSome say "refresh the page" because the dashboard shares these endpoints.
error_code, codestring
The stable code to branch on. Read error_code ?? code.
DetailsSome errors use code instead of error_code, some use both. Codes are case-sensitive, and a few conditions have an upper and a lower case form from different endpoints (both are in the table below).
…
Extra fields where they apply, such as next_allowed_at, alternatives, field, balance_cents.
  • A 400 with no code is a malformed request: fix the body, don't retry.
  • An unknown path or an unsupported method returns an HTML 404 page, not JSON. A missing resource on a real path returns a JSON 404 with a code.
  • Not successes: 200 empty_merge_field, 200 generation_refused and 202 send_outcome_unknown.

When to retry

ResponseWhat to do
429 rate_limited, throughput_exceeded, RATE_LIMITED, validation_in_progressNothing ran. Wait Retry-After (a minute if absent), retry with the same key.
DetailsA 429 is never stored under the key.
429 daily_cap, number_daily_capA cap, not a speed limit, with no Retry-After: don't loop. Resets at midnight Pacific (America/Los_Angeles).
429 monthly_cap, 402 MONTHLY_CAP_REACHEDDon't loop. The cap uses a 30-day window, not a calendar month. Wait for capacity or complete registration with a paid number (see Sending caps).
429 ramp_cap, 409 RAMP_CAP_REACHEDWait for the warm-up to end (ramp_ends_at when present).
409 idempotency_in_progressWait a few seconds, retry with the same key.
500 send_failed, 500 hold_not_returned, any 503 on a sendNot sent, not charged. Retry with a new key (the old one replays the error).
DetailsFor hold_not_returned, support returns the held amount. Wait Retry-After first when it's there (60 seconds on provider_busy). ADMISSION_SHED and permission_check_unavailable come before the key is recorded, so the same key works for them too.
504 REQUEST_TIMEOUT_OUTCOME_UNKNOWN, or your client timed outMay still be running. Retry with the same key after a minute. A new key can send twice.
DetailsOnce the first request has finished, you get its real result with Idempotent-Replayed: true. Before that you get 409 idempotency_in_progress or the 504 again; wait and retry with the same key.
202 send_outcome_unknown, 500 INTERNAL_ERROR_OUTCOME_UNKNOWNMay have been delivered and charged. Decide whether a duplicate or a missed text is worse.
DetailsRetrying with the same key returns this same answer, and the API can't confirm the outcome (see Checking whether a text went out). Sending again with a new key can deliver it twice.
Other 4xxFix the request, send it with a new key.
422Don't retry. The number can't receive texts.

Checking whether a text went out

There is no lookup by Idempotency-Key, and GET /v1/messages lists refused and unconfirmed attempts too. A listed attempt is not proof of delivery. queued means handed to the carrier; delivered means delivery confirmed. unknown means carrier acceptance could not be confirmed: it may still have delivered.

Details
  • The list filters only on direction, to, from and status.
  • A send refused with an error (an opt-out, a cap, a send_failed) is saved as an outbound row with status: "failed", cost_usd: 0 and the same body. Finding the body in the list doesn't mean it was sent.
  • A 202 send_outcome_unknown attempt is recorded as unknown, separate from a definitive failure. A zero cost does not prove it was unsent or refunded. Later evidence may update the outcome; do not resend with a new key just because delivery is unconfirmed.
  • A failed row with cost_usd above 0 went out and the carrier didn't deliver it. See Failures Ready resends for you before you send it again.

Status codes

Standard HTTP meanings. The ones to know: 402 payment needed, 409 not sendable right now, 422 the number can't receive texts, 503 a safety check couldn't run (not sent, not charged), 504 a write may still go through.

Details: every status
  • 200 OK. 201 created (contacts). 202 accepted but not finished, or outcome unknown.
  • 400 bad request. 401 no or bad credentials. 402 payment needed. 403 not allowed (often: can't text this contact). 404 not found.
  • 406 unsupported API version. 408 timed out, nothing changed. 409 conflict, or not sendable now. 413 body over 1 MB. 422 number can't receive texts. 423 legal hold. 429 too fast or over a limit.
  • 500, 502 failed on our side. 503 a safety check couldn't run: not sent, not charged. 504 a write timed out and may still go through.
Response
{
  "success": false,
  "error": "This contact replied STOP and is opted out. They must reply START from their phone to opt back in.",
  "error_code": "opt_out_locked"
}
Branch on the code
const body = await res.json();
const code = body.error_code ?? body.code; // may be undefined on a malformed request

Every error code

Field: where the code is (error_code, code, both, or error; either: read error_code ?? code). Retry: Same key and After Retry-After reuse your Idempotency-Key; any other resend needs a new key.

CodeHTTPFieldRetry?What to do
empty_merge_field200error_codeAfter fixingA merge field is blank. Nothing sent.
DetailsResend with "confirm_empty_merge": true to send it with the blank.
generation_refused200error_codeAfter fixingAI drafts: the ask breaks texting rules. Charged, not refunded.
DetailsBody has "success": false and "refused": true.
payment_processing202eitherLaterThe registration payment is still processing.
send_outcome_unknown202error_codeNoMay have been delivered and charged.
DetailsWe couldn't confirm the carrier took it. A retry with the same key returns this answer again. See When to retry.
address_incomplete400codeAfter fixingBrand address missing a part.
DetailsOne of street, city, state, zip. error names what's missing.
area_code_invalid400codeAfter fixingarea_code is missing or not 3 digits.
attestation_required400errorAfter fixingTurning off tcpa or dnc_complainers needs confirmation_text: "I UNDERSTAND".
brand_required_first400error_codeAfter fixingRegister a brand before a campaign.
business_description_required400codeAfter fixingBrand needs business_description.
DetailsOne or two sentences on what the business does.
ca_not_provisioned400error_codeNoCanada sending not set up on this account.
categories_required400errorAfter fixingScrub categories: categories missing.
DetailsThis endpoint puts the code in error and the sentence in message.
CONTACT_UPDATE_FAILED400codeAfter fixingA value can't be saved (e.g. non-integer score).
CONTENT_BLOCKED400bothNoBlocked content. Nothing sent; a compliance fee may apply.
DetailsLink shorteners, prohibited or regulated content. error states the fee and reasons lists what was flagged. Other send paths use lower case content_blocked.
ein_name_mismatch400errorAfter fixingEIN doesn't match the business name.
DetailsThe body has title, message, steps, can_override.
idempotency_key_required400error_codeAfter fixingAPI-key send without Idempotency-Key. Not enforced yet.
DetailsReturned once Ready requires the header; today the send goes through. Add a new unique key per request.
idempotency_key_too_long400error_codeAfter fixingIdempotency-Key is over 255 characters.
invalid_assignee400codeAfter fixingOne of the ids isn't on your team.
INVALID_CONTACT_ID400codeAfter fixingUse the contact's id, not its phone number.
invalid_ein400codeAfter fixingein isn't 9 digits (XX-XXXXXXX or XXXXXXXXX).
invalid_field400codeAfter fixingWrong type; field names it.
invalid_from400error_codeAfter fixingfrom isn't one of your numbers.
INVALID_JSON400codeAfter fixingThe body isn't valid JSON.
invalid_number400bothAfter fixingThe phone number has fewer than 10 digits.
INVALID_PHONE400codeAfter fixingThe contact's phone number can't be read.
invalid_offset400error_codeAfter fixingoffset isn't a whole number, 0 or more.
invalid_phone400error_codeAfter fixingGET /v1/validate/:phone: path value over 40 characters.
DetailsA short value that isn't a number answers 200 with valid: false.
invalid_request400bothAfter fixingMissing or misshapen body; error says what.
invalid_status400codeAfter fixingMust be open, active, closed or archived.
invalid_sub_account_id400bothAfter fixingX-Sub-Account-Id isn't a numeric account id.
invalid_url400bothAfter fixingWebhook URL not usable.
DetailsA bad URL, not http or https, or a private or internal host.
invalid_use_case400error_codeAfter fixingUnknown campaign use_case. valid lists the allowed values.
MESSAGE_TOO_LONG400error_codeAfter fixingOver 25 segments, or 1,600 characters with an image.
DetailsOther send paths use lower case message_too_long.
NO_APPROVED_CAMPAIGN400error_codeNoThe number has no approved 10DLC campaign yet.
no_country_sender400error_codeNoNo number of yours can send to this country.
no_sender_number400error_codeAfter fixingNo active number to send from.
payment_intent_mismatch400eitherNoThe payment isn't for this vetting.
payment_intent_required400eitherAfter fixingBuying a number needs payment_intent_id.
DetailsRelated: payment_not_confirmed, payment_not_for_number, payment_not_taken.
payment_not_completed400eitherAfter payingThe registration payment didn't go through (status).
PHONE_NOT_EDITABLE400codeNoContact phone can't change. Create a new contact.
phones_required400error_codeAfter fixingValidate needs phones: 1 to 1,000 numbers.
preferred_area_code_required400errorAfter fixingnumber_choice: "new" without preferred_area_code.
registration_approved400eitherNoApproved registrations can't be cancelled via API. Contact support.
REQUEST_BLOCKED400codeAfter fixingQuery string looks like code (HTML, script, SQL).
DetailsRemove it. Repeats can get your IP blocked (IP_TEMPORARILY_BLOCKED).
sub_account_header_needs_api_key400bothAfter fixingX-Sub-Account-Id needs an API key, not OAuth or session.
sub_account_header_not_supported400bothAfter fixingX-Sub-Account-Id works only on /v1 paths.
terms_required400bothAfter fixingReseller terms aren't accepted yet. Contact support to set this up.
too_many_numbers400bothAfter fixingScrub with more than 1,000 numbers.
too_many_phones400bothAfter fixingValidate with more than 1,000 numbers.
too_many_recipients400error_codeAfter fixingBulk send with more than 100 recipients.
unknown_category400errorAfter fixingScrub categories: unknown key.
DetailsAllowed: tcpa, dnc_complainers, federal_dnc, state_dnc.
unknown_field400codeAfter fixingField not accepted here. Remove it.
UNRENDERED_MERGE_TOKEN400error_codeAfter fixingA merge tag didn't fill. Not sent, not charged.
AUTH_REQUIRED401codeAfter fixingNo key, an empty variable, or lower case bearer.
INVALID_API_KEY401codeAfter fixingrsms_ key not valid.
DetailsRevoked, mistyped or cut off.
INVALID_TOKEN401codeAfter fixingNot an rsms_ key or valid OAuth token.
BILLING_PAST_DUE402codeAfter fixingPayment failed or plan cancelled, and no balance. Nothing ran.
DetailsUpdate your card (billing_url). FRAUD_HOLD (402) means the account is on hold: contact support.
card_declined402eitherAfter fixingThe saved card was declined. Also card_authentication_required.
CARD_REQUIRED402error_codeAfter fixingAdd a card in Settings → Billing.
insufficient_balance402error_codeAfter adding fundsBalance too low; see balance_cents, required_cents.
insufficient_credits402bothAfter adding fundsBalance can't cover the whole batch.
DetailsBulk send, validation. Nothing was sent or charged.
MONTHLY_CAP_REACHED402codeNoMonthly sending cap reached.
DetailsSame limit as monthly_cap (429) from other send paths.
PURCHASE_BLOCKED402codeNoNumber purchases blocked. Contact support.
DetailsUnpaid bill or account review.
SANDBOX_LIMIT402error_codeNoTrial free sends used up. Register to keep sending.
ACCOUNT_SUSPENDED403codeNoAccount suspended; every call refused. Contact support.
DetailsThe send path can also answer lower case account_suspended in error_code.
dnc_list403error_codeNoOn a do-not-call list your account screens against.
EXPORT_DISABLED403codeNoOnly the account owner can export contacts on this account.
global_blocklist403error_codeNoThe number is on Ready's blocklist.
IP_TEMPORARILY_BLOCKED403codeLaterIP blocked 15+ minutes.
DetailsToo many over-limit requests without a valid key.
opt_out_locked403error_codeNoRecipient opted out. Nothing sent, not charged.
DetailsSTOP, marked opted out, or Do Not Disturb for texts; reason says which. On PUT /contacts/:id: you tried to re-enable texting for a STOP-locked contact. Only the contact texting START lifts a STOP.
number_not_assigned403error_codeNoSending number not assigned to you. Nothing sent.
DetailsNot assigned to this user or client account.
opted_out403error_codeNoPUT /contacts/:id: re-enabling texting on an opted-out contact.
DetailsThe contact is opted out but not STOP-locked (for example marked Do Not Disturb). The contact must text START.
payment_not_owned403eitherNoThe payment belongs to another account.
permission_denied403bothNoThe key's user lacks the permission this endpoint needs.
quiet_hours403error_codeLaterOutside the recipient's allowed hours.
DetailsOther send paths; /v1/messages answers 409 QUIET_HOURS_BLOCK.
READ_ONLY_SESSION403error_codeNoView-only access can't make changes or spend money.
REQUEST_BLOCKED403codeNoUser-Agent looks like a scanner. Send your own.
reseller_not_approved403bothLaterMarkup set while reseller setup is in review.
DetailsWith no markup setup at all, the same call answers 403 forbidden. See Client accounts.
sub_account_forbidden403bothNoThat account isn't a client account you manage.
sub_account_suspended403bothNoThat client account is suspended.
test_key_not_allowed403errorNoA test key called a non-simulated endpoint. Nothing changed.
tcpa_block403error_codeNoKnown TCPA litigator.
CONTACT_NOT_FOUND404codeNoNo contact with this id on your account.
CONVERSATION_NOT_FOUND404codeNoNo conversation with this id on your account.
message_not_found404bothNoNo such message. Body has status: "unknown".
not_found404codeNoConversation not found (from PUT /conversations/:id/assign).
not_rejected404eitherNoResubmit works only on a rejected registration.
NUMBER_NOT_FOUND404codeNoNo number with this id on your account.
payment_not_found404eitherNoNo such registration payment on your account.
registration_not_found404error_codeNoNo registration with this id on your account.
sub_account_not_found404bothNoNo account has that id.
unsupported_api_version406error_codeAfter fixingUnserved Accept-Version; supported lists versions.
REQUEST_TIMEOUT408codeYesRead or upload over 30 s. Nothing changed.
already_vetted409error_codeNoThe brand already has this vetting tier or higher.
area_code_unavailable409error_codeAfter fixingArea code out of stock; see alternatives.
CONTACT_CHANGED409codeAfter fixingContact opted out mid-save. Read it again.
CONTACT_EXISTS409codeNoPhone already a contact (GET /contacts?search=).
duplicate_brand409error_codeNoYou already have this brand (existing_reg_id).
duplicate_send409error_codeNoSame text, same number, within 60 s, no key.
duplicate_use_case409error_codeNoA campaign with this use case exists.
DetailsYour brand created a MIXED campaign for you.
idempotency_in_progress409error_codeSame keyFirst request still running. Wait a few seconds.
idempotency_outcome_unknown409error_codeNoFirst request with this key ran over 20 minutes; outcome unknown.
DetailsSee Checking whether a text went out.
INVALID_SENDER409error_codeNoSending number can't send now. Try another.
needs_transfer_confirm409flagAfter fixingNumber is on another brand. Resend with "transfer_confirm": true.
DetailsNot a code: the 409 body has needs_transfer_confirm: true, number, current_brand and message.
number_activating409error_codeLaterThe sending number is still being set up.
number_not_active409error_codeNoThe sending number isn't active.
NUMBER_NOT_ASSIGNED_CONFIRM409error_codeAfter fixingNumber not assigned to the contact's owner (dashboard).
NUMBER_RESERVED409codeNoThis number can't be released right now.
paid_not_filed409errorNoPaid, not filed. Don't pay again; contact support.
payment_intent_already_used409error_codeNoEach payment_intent_id buys exactly one number.
purchase_in_progress409eitherLaterAnother number purchase is running. Also campaign_number_limit.
QUIET_HOURS_BLOCK409codeLaterRecipient's quiet hours.
DetailsBody has next_allowed_at, reason, recipient_tz, can_override. When can_override is true, send again with override_quiet_hours.
RAMP_CAP_REACHED409codeLaterNew campaign's warm-up cap used up.
DetailsBody has cap, count, ramp_ends_at. Same limit as ramp_cap (429) from other send paths.
route_unavailable409error_codeLaterNo route to this recipient right now.
scrub_off409bothAfter fixingScrubbing is off. Turn it on in Settings → Compliance.
PAYLOAD_TOO_LARGE413codeAfter fixingThe body is over 1 MB.
auto_reply_suppressed422error_codeNoAn automatic reply was held back.
INVALID_DESTINATION422error_codeNoNot a valid phone number. Nothing sent, not charged.
landline_no_sms422error_codeNoThe number is a landline.
number_disconnected422error_codeNoA deactivation record or previous delivery failures blocked this attempt. This does not confirm the number is currently disconnected. Read the response’s error for the specific explanation.
PROVIDER_REJECTED422error_codeNoThe carrier refused this text. Not charged.
sms_invalid422error_codeNoThe number was marked as unable to receive texts.
undeliverable422error_codeNoThe carrier can't deliver to this number.
LITIGATION_HOLD423codeNoLegal hold: the contact can't be deleted.
daily_cap429error_codeNext dayAccount daily cap reached. No Retry-After.
DetailsOff unless Ready set one for your account. Resets at midnight Pacific time.
duplicate_submission429error_codeNoSame registration in the last 2 minutes (existing_reg_id).
monthly_cap429error_codeNoMonthly cap reached. No Retry-After.
DetailsSame limit as MONTHLY_CAP_REACHED, which /v1/messages answers.
number_daily_cap429error_codeNext daySending number's daily cap reached. No Retry-After.
DetailsYou set it per number. Resets at midnight Pacific time. Send from another number or wait.
ramp_cap429error_codeLaterWarm-up cap. No Retry-After.
DetailsSame limit as RAMP_CAP_REACHED. Wait for the warm-up to end.
rate_limited429eitherAfter Retry-AfterToo many requests for this account.
DetailsCarrier pacing on a send: in error_code with retry_after_ms and cap_per_sec. Per-minute limits: in error_code (about 120 a minute) or code (the 6,000 sends a minute ceiling, 140 bulk calls a minute), with no retry_after_ms: wait Retry-After, or a minute.
RATE_LIMITED429codeAfter Retry-AfterToo many from this IP; see retry_after_seconds.
throughput_exceeded429bothAfter Retry-AfterBatch over your carrier pace. Nothing sent.
DetailsBody has retry_after_ms and cap_per_sec.
validation_in_progress429bothAfter Retry-AfterThe same numbers are already being validated.
buy_incomplete500error_codeNoAdded but setup unfinished. Don't rebuy; check GET /v1/numbers.
generation_failed500eitherYesWriting drafts failed. Refunded.
hold_not_returned500error_codeNew keyNot sent, but the hold wasn't returned yet.
DetailsSupport refunds the held amount; you don't need to do anything.
internal_error500bothCheck firstFailed on our side.
DetailsOn billed calls (validate, scrub), check your balance before you retry.
INTERNAL_ERROR_OUTCOME_UNKNOWN500codeNoA write failed on our side; outcome unknown.
DetailsTreat a send like 202 send_outcome_unknown. Reads answer INTERNAL_ERROR instead.
RELEASE_INCOMPLETE500codeCheck firstThe number release didn't finish. Check GET /v1/numbers/:id.
resubmit_unconfirmed500eitherCheck firstDon't resubmit again yet; poll GET /v1/registration.
server_error500codeCheck firstFailed on our side.
send_failed500error_codeNew keyNot sent, not charged. Retry with a new key.
DetailsWait a minute. The same key replays this error. In bulk results, also used when a recipient's refusal had no code at all: read error.
BILLING_CANCEL_FAILED502codeLaterMonthly fee not stopped, so nothing released.
checkout_unconfirmed502error_codeNoCheckout unconfirmed. Check GET /v1/numbers first.
payment_unknown502error_codeNoRegistration payment unconfirmed. Don't resubmit; poll GET /v1/registration.
subscription_cancel_failed502eitherLaterMonthly fee stop unconfirmed, so it was kept.
ADMISSION_SHED503codeSame keyOverloaded; nothing done. Wait Retry-After (1 s) and retry.
AUTH_CHECK_UNAVAILABLE503codeYesWe couldn't check your key. Nothing ran.
carrier_capacity_busy503error_codeNew keyCarrier capacity busy. Not sent, not charged.
checkout_unavailable503eitherLaterCheckout unavailable. Nothing charged.
DetailsAlso availability_check_unavailable, cap_check_unavailable.
contact_unavailable503error_codeNew keyThe contact couldn't be read. Nothing sent.
could_not_retrieve_payment_intent503errorYesVetting payment unchecked. Don't pay again; retry.
DetailsRetry the confirm call. The same code with 400 means the payment wasn't found: contact support.
gate_check_failed503error_codeNew keySafety check couldn't run. Not sent, not charged.
payment_check_unavailable503eitherYesWe couldn't check the registration payment.
payments_unavailable503eitherLaterRegistration payments are unavailable. Nothing charged.
permission_check_unavailable503bothNew keyWe couldn't check the key's permissions. Nothing ran.
provider_busy503error_codeNew keyCarrier is limiting our rate. Not sent, not charged.
DetailsHas Retry-After: 60: retry after that.
sending_paused503error_codeLaterSending is paused. Nothing sent, not charged.
tcpa_not_configured503bothLaterScrubbing unavailable. Nothing scrubbed or charged.
throughput_unavailable503error_codeLaterThroughput couldn't be read.
validation_unavailable503bothLaterNumber validation isn't available right now. Nothing charged.
REQUEST_TIMEOUT_OUTCOME_UNKNOWN504codeSame keyA write ran over 30 s and may still go through.
DetailsWith an Idempotency-Key, retry with the same key after a minute to get its result. Elsewhere, check whether it worked before you retry.

#Conventions

Pagination

Offset lists (GET /contacts, /conversations, /conversations/:id/messages, /billing/transactions, /sms/logs) take limit and offset. Cursor lists (GET /v1/messages, /v1/opt-outs, /v1/sub-accounts) take limit and before_id: pass the previous page's next_before_id. They ignore offset, so a client that sends it gets page one forever.

Stop when has_more is false (hasMore on /sms/logs), not on a short page or on next_before_id. /contacts has only total: stop on a page with fewer rows than limit.

Details
  • limit defaults to 50 and is capped at 1,000 (500 for thread messages; 200 for transactions, where the default is 20). Larger values are capped without an error. 0, negative or non-numeric values fall back to the default, except on GET /billing/transactions, where limit=0 returns an empty page.
  • offset must be a whole number, 0 or more. Other values return 400 invalid_offset on /contacts and /conversations.
  • next_before_id is set on the last page too, and is null when the page is empty. has_more can be true on a page that is exactly full, so the last page may be empty.
  • total can be an estimate on large or slow queries, can be up to 2 minutes old, and can show 1 when nothing matches. Use it for display, not to end a loop.
EndpointStylePaging fields next to dataMax limit
GET /contactsoffsettotal1,000
GET /conversationsoffsethas_more, total, total_capped, count_cap, counts1,000
GET /conversations/:id/messagesoffsetnone500
GET /billing/transactionsoffsettotal, summary200
GET /sms/logsoffsethasMore, total (always null)1,000
GET /v1/messagescursorhas_more, next_before_id200
GET /v1/opt-outscursorhas_more, next_before_id500 (default 100)
GET /v1/sub-accountscursorhas_more, next_before_id200
Offset style
GET /conversations?limit=100&offset=0
GET /conversations?limit=100&offset=100
# stop when has_more is false
Cursor style
GET /v1/messages?limit=200
GET /v1/messages?limit=200&before_id=<next_before_id>
# stop when has_more is false

Rate limits

A 429 from these limits means nothing ran: wait Retry-After seconds and retry with the same Idempotency-Key. The sending caps (daily_cap, number_daily_cap, monthly_cap, ramp_cap) also answer 429 but have no Retry-After and won't clear in a retry loop: see When to retry and Sending caps.

WhatLimitCountedOver it
Sending (/v1/messages, /sms/send)Paced to your carrier throughput for AT&T and unknown-carrier recipients (3 a second if your cap is unknown). Hard ceiling 6,000 sends a minute.
DetailsReady holds a request up to 1.5 s before refusing, for up to 40 waiting requests per server; past that a send is refused at once.
Per account (pacing is counted per server)Pacing: 429 rate_limited in error_code, with Retry-After. Ceiling: 429 rate_limited in code; wait a minute.
DetailsThe pacing body has retry_after_ms and cap_per_sec; Retry-After is retry_after_ms rounded up to whole seconds. The ceiling response has no retry_after_ms.
Bulk send, message history, validate, scrub (/v1/messages/bulk, GET /v1/messages, /v1/validate, /v1/scrub)About 120 requests a minute, plus 140 bulk calls a minutePer account (the 120 is counted per server, so it's approximate)429 rate_limited (in error_code for the 120, in code for the 140). Bulk also gets 429 throughput_exceeded, below.
Number purchase/phone-numbers/checkout: 20 a minute. POST /v1/numbers/checkout and POST /v1/numbers: 120 a minute. Shared ceilings of 700 checkouts and 140 buys a minute.Per account429 rate_limited
Contact exports6 an hour, shared with the dashboardPer account429
Everything1,200 requests a minute with a valid key, 300 without onePer IP address429 RATE_LIMITED with retry_after_seconds

Repeated 429s without a valid key block the IP for 15 minutes or longer (403 IP_TEMPORARILY_BLOCKED). A dashboard session and an API key on the same account share the per-account limits.

Details

For example a mistyped key in a retry loop. Requests with a valid key are throttled but never blocked.

Bulk isn't faster. POST /v1/messages/bulk counts every recipient against the same per-second pace before it sends anything, and refuses an over-pace batch whole: 429 throughput_exceeded with Retry-After.

Details

Bulk counts every recipient on every carrier; single sends only count AT&T and unknown-carrier recipients. The pacing 429 on a single send suggests bulk, but bulk won't get you past your carrier pace.

Sending caps

  • Daily caps: none on your account unless Ready sets one (429 daily_cap). You can cap a number yourself (429 number_daily_cap). Both reset at midnight Pacific time (America/Los_Angeles), whatever your account's time zone.
  • Unregistered accounts created since June 23, 2026: 500 texts per 30-day cap window until they have an approved campaign and a paid number (402 MONTHLY_CAP_REACHED on /v1/messages). No daily cap. The send counter resets 30 days after its window starts, not on the first of the month; recent-send counts use the last 30 days.
  • Warm-up: after brand and campaign approval, see Warm-up limits.
  • T-Mobile gives your brand a daily tier for texts to T-Mobile numbers (GET /v1/registration/throughput). Ready doesn't hold or refuse /v1/messages or bulk sends for it, so a send over your tier isn't refused by Ready.

Headers. Every response has X-RateLimit-Limit, X-RateLimit-Remaining (approximate) and X-RateLimit-Reset (Unix seconds) for the limit that applied. On sends, rely on Retry-After.

Details

On sends, X-RateLimit-Policy: 3;w=1 is your carrier pace in texts per second, and Limit and Remaining can come from two different limits.

Response
{
  "success": false,
  "error": "You're sending to AT&T faster than your numbers allow (about 3 texts a second), so this text wasn't sent. Wait 2 seconds and try again, or send in one call with POST /v1/messages/bulk.",
  "error_code": "rate_limited",
  "retry_after_ms": 1500,
  "cap_per_sec": 3
}

Retries and idempotency

Send a new Idempotency-Key per message, and reuse it only to retry that message. Any unique string up to 255 characters (a UUID is easiest; longer is 400 idempotency_key_too_long). A retry with the same key returns the original response with Idempotent-Replayed: true, without sending or charging twice.

  • Where: POST /v1/messages, /v1/messages/bulk, /sms/send, /v1/numbers/checkout, /phone-numbers/checkout, POST /v1/numbers, /v1/registration/:id/resubmit, /v1/validate and /v1/scrub.
  • A key is tied to its first request. The same key with a different body returns the first result and does not send the new one. There's no mismatch error.
  • Errors are stored too (all but 429). After fixing a request, or after a 5xx that says nothing was sent, use a new key. After a 504 or your own timeout, reuse the key (When to retry).
    Details: errors that aren't storedErrors are stored once the key is recorded, including a 202, a 500, a 503 and a 504. These are refused before the key is recorded, so they aren't stored and a corrected retry with the same key goes through:
    • On any endpoint: malformed JSON (INVALID_JSON), PAYLOAD_TOO_LARGE, idempotency_key_too_long, authentication and account errors (AUTH_REQUIRED, INVALID_API_KEY, INVALID_TOKEN, ACCOUNT_SUSPENDED, AUTH_CHECK_UNAVAILABLE), X-Sub-Account-Id errors, permission_denied, IP_TEMPORARILY_BLOCKED, REQUEST_BLOCKED (400 or 403), REQUEST_TIMEOUT (408, the body never arrived), ADMISSION_SHED (503), and on /v1 paths unsupported_api_version.
    • On POST /v1/messages: a missing to or body, invalid_field and invalid_from.
    • On POST /v1/numbers and POST /v1/numbers/checkout: invalid_field. With a live key, the corrected retry is a real purchase.
  • Keys are scoped per account and per endpoint, and remembered for 7 days.
  • A retry while the first request runs gets 409 idempotency_in_progress. Unfinished after 20 minutes, it gets 409 idempotency_outcome_unknown: treat it like 202 send_outcome_unknown (Checking whether a text went out).

Duplicate guard. Without a key, an identical text (same recipient, body and media) within 60 seconds is refused: 409 duplicate_send. To send it twice on purpose, give each request its own key.

Versioning

/v1 responses carry X-API-Version: 1. Pin it with Accept-Version: 1; an unserved version returns 406 unsupported_api_version with a supported list. Paths outside /v1 aren't versioned.

Timeouts

Requests time out after 30 seconds (90 for number checkout and purchase). A read, or an upload that never finished, returns 408 REQUEST_TIMEOUT: nothing changed. A write that may have gone through returns 504 REQUEST_TIMEOUT_OUTCOME_UNKNOWN and Ready keeps working on it.

Details

On the endpoints that take an Idempotency-Key, retry with the same key after a minute to get its real result; a new key can do it twice. If your own client times out first, do the same. Elsewhere, check whether it worked before you retry.

Timestamps, money and ids

  • Timestamps: ISO 8601 in UTC with milliseconds (2026-10-01T20:32:03.473Z), or null.
  • _cents amounts are integer cents. _usd amounts and rates are US dollars. On /billing/balance, balance is a count of texts, not money.
  • Ids are opaque. Message ids are strings up to 64 characters (digits for texts you send, often alphanumeric for texts you receive): don't parse them.
  • Phone numbers come back in E.164 (+12125550199).

Response headers

HeaderWhat it is
X-Request-IdA UUID on every response. Include it when you contact support.
X-API-Version1 on /v1 responses.
X-RateLimit-Limit, -Remaining, -ResetThe limit that applied (see Rate limits).
X-RateLimit-PolicyOn sends: your carrier pace, N;w=1.
Retry-AfterOn 429 (and some 503s): seconds to wait.
Idempotent-Replayedtrue when the response is a stored replay.