ready. Try Ready →

API

A REST API over HTTPS for SMS, contacts, billing, and webhooks — plus a Zapier app and an MCP server for AI agents. Every response is JSON with a top-level success boolean.

Overview

All endpoints are relative to the production base URL. There is no sandbox — all documented endpoints are production.

Base URLCopy
https://api.tryready.com

Success responses include a data field; error responses include an error string. Requests and responses are JSON (Content-Type: application/json).

Authentication

Two methods are supported. Use API keys for direct server-to-server calls you own; use OAuth 2.0 for third-party integrations (Zapier, partner apps).

API keys

Generate keys in Settings → Integrations ("Generate an API key"). Pass either as a bearer token or an X-API-Key header.

HTTPCopy
Authorization: Bearer rsms_your_api_key_here
X-API-Key: rsms_your_api_key_here

Keys prefixed rsms_ are general API keys; keys prefixed zap_ are Zapier-scoped keys.

Walkthrough: create an API key3 steps
  1. Open Settings (the gear, top-right) → Integrations.
  2. Find the developer / API section and click Generate new key.
  3. Copy the key — it's shown once and starts with rsms_. Send it as Authorization: Bearer <key> or an X-API-Key header on your requests.
Consent is required by law. Only message contacts who gave prior express written consent (TCPA). Ready processes STOP and other opt-outs automatically — see Compliance.

OAuth 2.0

Authorization-code flow used by the published Zapier integration. Tokens from /zapier/oauth/token are JWTs — pass them as Authorization: Bearer <token>. See Zapier surface.

Errors

Errors return a non-2xx status and a JSON body with an error string and, where useful, a stable error_code you can branch on:

JSON — error responseCopy
{
  "success": false,
  "error": "This contact replied STOP and is opted out.",
  "error_code": "opt_out_locked"
}

HTTP status codes:

  • 400 — bad request (missing/invalid field). error_code: invalid_number, invalid_url, MESSAGE_TOO_LONG
  • 401 — missing or invalid credentials
  • 402 — insufficient balance for a paid action. error_code: insufficient_credits
  • 403 — blocked / not permitted. error_code: opt_out_locked (recipient replied STOP), global_blocklist (recipient blocked platform-wide), permission_denied
  • 404 — resource not found
  • 429 — rate limited (back off + retry)
  • 502 — carrier rejected the message outright (error_code: OTP_NOT_DELIVERED and similar)
What a 2xx means (important). A success on POST /sms/send means the message was accepted for sendingstatus: "queued" — not that it was delivered. Opt-outs and platform blocklists are rejected up front with a 403 (so you know immediately). But a number we can't reach — e.g. a landline — is only known once the carrier reports back. To confirm real delivery, read the delivery status: poll GET /sms/logs / GET /v1/messages (or subscribe to the message_delivered / message_failed webhooks). Delivery status values: queueddelivered, or failed / undelivered.

Conventions

Rules that apply across every endpoint.

Pagination

List endpoints — /contacts, /sms/logs, /conversations — take limit and offset query params and return the page in data. Keep requesting until you get a short or empty page.

Rate limiting

A 429 means you're going too fast — back off and retry with exponential backoff. Authentication and one-time-code endpoints are tightly limited (a handful of attempts per 15 minutes) and bulk exports are capped per hour. Sending isn't request-quota'd — it's gated by your credit balance.

Retries & idempotency

/sms/send is not idempotent. If a request times out the message may already be queued, so a blind retry can double-send. Before retrying, confirm with /sms/logs (or key off the returned message_id) rather than re-POSTing.

SMS

Send messages, query history, and generate AI-written copy.

POST/sms/sendSend an SMS message

Body

tostringRecipient in E.164 (e.g. +15551234567). required
messagestringMessage body. required
media_urlstringOptional — attach a media URL to send as MMS.
fromstringOptional — send from one of your Ready numbers by its E.164 number (e.g. +15551234567); we resolve it to the number's id for you. Use this or from_phone_number_id.
from_phone_number_idintOptional — send from a specific one of your Ready numbers (its id). Omit both this and from and Ready auto-picks the right number for this contact.
from_strategystringOptional — how Ready picks the number when you don't pin one: local (match the recipient's area code — the default), even / deliverability (spread across your numbers, least-loaded first), or random. Ignored if from_phone_number_id is set.

Example

cURLCopy
curl -X POST https://api.tryready.com/sms/send \
  -H "Authorization: Bearer rsms_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+15551234567",
    "message": "Hello from Ready!"
  }'

Response 200

JSONCopy
{
  "success": true,
  "data": {
    "message_id": "3f0a9c2e-7b41-4e8a-9c1d-2b6f5a0e1d34",
    "segments": 1,
    "status": "queued",
    "credits_remaining": 4823
  }
}

status starts as queued; the carrier confirms delivered / failed within ~5–30s. Use message_id to poll GET /sms/logs or GET /v1/messages, or subscribe to delivery webhooks. Add media_url to send as MMS. Cost = segments × (your per-segment rate + the per-segment carrier fee) (both returned by GET /v1/account).

POST/v1/messages/bulkSend one body to many recipients

Up to 100 recipients per call. Each recipient runs the full send path independently (opt-outs, blocklist, and per-message billing all apply per recipient), so you get an accurate per-recipient result.

Body

tostring[]Recipients in E.164. required
bodystringMessage body. required
fromstringOptional — one of your numbers to send from.

Response 200

JSONCopy
{
  "success": true,
  "data": {
    "total": 2,
    "sent": 1,
    "failed": 1,
    "results": [
      { "to": "+15551234567", "status": 200, "sent": true, "message_id": "3f0a9c2e-…", "segments": 1 },
      { "to": "+15559876543", "status": 403, "sent": false, "error": "opted out", "error_code": "opt_out_locked" }
    ]
  }
}
GET/sms/logsMessage history

Query

limitintMax rows returned.
offsetintPagination offset.
directionstringinbound or outbound.
statusstringFilter by delivery status.
searchstringMatch a number or message text.
POST/sms/generateAI-generate SMS copy — 5 variants

Body

descriptionstringWhat the message should say. required

Returns 5 message variants. Costs 5 credits (requires available balance).

Contacts

Create, read, update, and delete contacts.

GET/contactsList contacts with filters

Query

searchstringName, phone, or email.
tagstringFilter by tag.
compliancestringoptout or dnc — return only suppressed contacts. See Suppression.
POST/contactsCreate a contact

Body

phonestringE.164 phone. required
first_namestringOptional.
emailstringOptional.
PUT/contacts/:idUpdate a contact

Send any updatable fields (first_name, last_name, email, tags, pipeline_stage).

DELETE/contacts/:idDelete a contact

Permanently removes the contact and its conversation history.

Conversations

The Inbox is one thread per contact. Read threads and their messages here; to reply, send to the contact with POST /sms/send — it lands in the same thread.

GET/conversationsList threads (paginated)

Query

limit / offsetintPagination.
searchstringName or phone (fast); free-text body search is bounded.
line_idsstringRestrict to specific sending numbers.
GET/conversations/:id/messagesMessages in a thread

The ordered message history for one conversation.

GET/conversations/by-contact/:contactIdThread for a contact

Look up the conversation for a given contact id.

PUT/conversations/:id/assignAssign to team members

Body

assignee_idsint[]Team member ids to assign this thread to.
PUT/conversations/:id/statusSet thread status

Open or close the thread in the workflow.

PUT/conversations/:id/readMark read

/unread, /archive, /unarchive, and /star follow the same shape.

Suppression & scrubbing

Ready suppresses and scrubs automatically — there's no endpoint to opt someone out or to scrub a list. Instead, every contact carries its compliance state, which you read back through /contacts.

Opt-outs are automatic

Inbound STOP / UNSUBSCRIBE (and the other standard keywords) are honored the moment they arrive: the contact is locked and future sends to it are blocked. You're also notified via the contact_opted_out webhook.

TCPA litigator scrubbing

Known-litigator scrubbing runs automatically on import and before sends. Choose which categories are enforced with GET / POST /compliance/scrub-categories; flagged contacts are marked is_litigator with a tcpa_status.

Reading suppression state

Each contact from /contacts carries opt_out_locked, status (opted_out), dnd_sms, opt_out_source, opt_out_at, is_litigator, tcpa_status, and sms_invalid. To pull just a suppression set, filter the list:

GET/contacts?compliance=optoutHonored opt-outs

Everyone who replied STOP or is marked DND for SMS (opt_out_locked / status='opted_out' / dnd_sms).

GET/contacts?compliance=dncFull do-not-contact set

The broadest suppression set — opt-outs plus email/call DND, TCPA litigators, and invalid numbers.

You don't need to pre-check before sending. Ready blocks sends to suppressed contacts server-side, so an opted-out number can't be messaged even by mistake. Use the filters above for reporting and list hygiene.

Phone numbers

List, search, buy, and release the numbers on your account — each returns clean JSON. Buying is instant for in-stock area codes.

GET/v1/numbersList your numbers

Response 200

JSONCopy
{
  "success": true,
  "data": [
    { "id": 8412, "phone_number": "+14155551638", "status": "active", "created_at": "2026-07-29T00:12:04Z" }
  ]
}
GET/v1/numbers/availableSearch buyable numbers

Query

area_codestring3-digit US/CA area code. required

Response 200

JSONCopy
{ "success": true, "available": true, "numbers": [ { "phone_number": "+14155550200" } ] }
POST/v1/numbersBuy a number

Body

area_codestring3-digit area code to provision from. required
payment_intent_idstringPays the number fee. required — get one from POST /phone-numbers/checkout (charges your card on file).
A number is a paid line ($5/mo + a one-time setup fee), so buying is a two-step call:
1. POST /phone-numbers/checkout → charges your card on file, returns a payment_intent_id.
2. POST /v1/numbers with { area_code, payment_intent_id } → provisions the number.
Each payment_intent_id provisions exactly one number.

Response 200

JSONCopy
{
  "success": true,
  "instant": true,
  "id": 2647,
  "phone_number_id": 2647,
  "phone_number": "+14155551638",
  "pending_provisioning": false
}

Instant provisions (covered area codes) return the number's id right away — use it as from_phone_number_id. Async provisions omit id (the number is created a moment later) and return an infobip_request_id to match via GET /v1/numbers. Out-of-stock area codes return a 400 with alternatives.

DELETE/v1/numbers/:idRelease a number

Frees the number and stops its monthly fee. Returns { "success": true }.

GET/v1/messagesMessage history

Query

directionstringinbound or outbound.
tostringFilter by recipient number.
statusstringFilter by delivery status.
limitintPage size (max 200, default 50).
before_idintCursor — pass next_before_id from the previous page.

Response 200

JSONCopy
{
  "success": true,
  "data": [
    {
      "id": "3f0a9c2e-7b41-4e8a-9c1d-2b6f5a0e1d34",
      "direction": "outbound",
      "from": "+14155551638",
      "to": "+15551234567",
      "body": "Hello from Ready!",
      "status": "delivered",
      "segments": 1,
      "cost_usd": 0.0245,
      "media_url": null,
      "created_at": "2026-07-29T00:12:10Z",
      "delivered_at": "2026-07-29T00:12:14Z"
    }
  ],
  "has_more": false,
  "next_before_id": null
}

Account & pricing

Read your balance and the exact rates you're billed at — so you can compute cost client-side.

GET/v1/accountBalance + your billed rates

Response 200

JSONCopy
{
  "success": true,
  "data": {
    "balance_usd": 481.20,
    "balance_cents": 48120,
    "plan": null,
    "sms_rate_usd_per_segment": 0.014,
    "carrier_fee_usd_per_segment": 0.0045
  }
}

Cost of a message = segments × (sms_rate_usd_per_segment + carrier_fee_usd_per_segment). These are your account's actual rates (including any negotiated rate).

GET/v1/opt-outs/:phoneCheck if a number is opted out

Free — checks your own opt-out list. Sends to opted-out numbers are already blocked server-side (403 opt_out_locked); use this if you want to know before sending.

Response 200

JSONCopy
{ "success": true, "data": { "phone": "+15551234567", "opted_out": false, "since": null } }
GET/v1/opt-outsList opted-out numbers

Paginated list of every number that opted out on your account (limit, default 100).

A2P registration

Register your business with the carriers (A2P / 10DLC) so your texts deliver at full throughput — end-to-end over the API. The flow: register a brand → register a campaign → confirm payment → poll status. Optionally vet the brand to raise your limits. Registration is paid, so a submit returns a payment_intent_id you confirm the same way you buy a number.

Test first. A rsms_test_ key simulates every write below — it validates your request and returns { "test": true, "charged": false } without creating a registration or charging a card.
GET/v1/registration/use-casesValid use-cases, required fields, pricing

Everything you need to build a valid request — call this first.

Response 200

JSONCopy
{
  "success": true,
  "data": {
    "use_cases": ["MARKETING","CUSTOMER_CARE","MIXED","DELIVERY_NOTIFICATIONS","ACCOUNT_NOTIFICATIONS","FRAUD_ALERTS"],
    "brand_required": ["company_name","ein","entity_type","industry","website","street","city","state","zip","email","phone","first_name","last_name"],
    "campaign_required": ["campaign_name","use_case","campaign_description"],
    "pricing_usd": { "brand": { "setup": 35, "monthly": 10 }, "campaign": { "setup": 0, "monthly": 20 } },
    "consent_required": true
  }
}
POST/v1/registration/brandRegister a brand

Body

company_namestringLegal business name. required
einstringFederal EIN (tax ID). required
consentboolSet true to attest you have prior express written consent to text your recipients. required
entity_type, industry, website, street, city, state, zip, email, phone, first_name, last_namestringBusiness + contact details. Strongly recommended — carriers reject thin brands.

Response 200

JSONCopy
{
  "success": true,
  "data": { "client_secret": "pi_..._secret_...", "amount": 3500, "registration_id": 481 }
}

Confirm the client_secret with Stripe (or call POST /v1/registration/confirm), then poll GET /v1/registration.

POST/v1/registration/campaignRegister a campaign

Body

campaign_namestringrequired
use_casestringOne of the use-cases enum. required
campaign_descriptionstringWhat you text and why. Strongly recommended.
consentboolAttestation, as above. required
Register a brand first — a campaign with no brand returns 400 brand_required_first.
POST/v1/registration/confirmConfirm payment → submit to carrier

Confirms a card payment so the registration submits immediately (the payment webhook also does this async). Body: { payment_intent_id }. Returns { "success": true, "registration_id": 481, "type": "brand" }.

GET/v1/registrationBrand + campaign status

Poll this to know when you're approved and clear to send at registered rates.

Response 200

JSONCopy
{
  "success": true,
  "data": {
    "brand": { "id": 481, "status": "approved", "carrier_status": "VETTED_VERIFIED", "approved_at": "2026-07-28T20:00:00Z" },
    "campaign": { "id": 482, "status": "rejected", "carrier_status": null, "approved_at": null,
                  "name": "Dealership outreach", "rejection_reason": "Sample message lacks opt-out language" },
    "can_send_registered": false
  }
}

status: not_started · pending · approved · rejected. Rejected regs include a rejection_reason — fix and POST …/resubmit. can_send_registered flips true once the campaign is approved.

GET/v1/registration/deniedFull rejection detail + AI fix

The latest rejected reg with carrier reasons, AI ai_fix suggestions, and the reg_id to resubmit. Returns { "denied": false } when nothing's rejected.

POST/v1/registration/:id/resubmitResubmit a rejected registration

Fix and resubmit reg :id. Optional body { fields: {…} } overrides. Returns the new status.

POST/v1/registration/:id/vetBrand vetting (raises your limits)

External vetting scores your brand higher with the carriers → higher daily throughput. :id is the brand's registration id.

Body

vetting_typestringSTANDARD or ENHANCED (higher tier, higher limits). Defaults to STANDARD.
Paid, one-time. Returns a client_secret + amount_cents; confirm with POST /v1/registration/:id/vet/confirm { payment_intent_id }. An already-Enhanced brand returns 409 already_vetted.
POST/v1/registration/:id/vet/confirmConfirm the vetting fee

Body: { payment_intent_id }. Applies the vetting to the brand.

POST/v1/registration/ein-lookupValidate / enrich a business

Look up a business by name to confirm its EIN + address before submitting a brand (reduces rejections). Body: { company_name, state?, street?, city?, zip? }. Rate-limited.

GET/v1/registration/throughputYour live sending capacity

24-hour volume, rejected segments (+ estimated $ loss), your T-Mobile daily cap / tier / next tier, and your warm-up ramp state. Use it to pace sends and to decide whether to vet.

Response 200

JSONCopy
{
  "success": true,
  "data": {
    "total_24h": 1840, "rejected_24h": 12, "reject_pct": 0.7,
    "tmobile": { "daily_cap": 2000, "tier": "T", "next_cap": 4000, "sent_today": 640, "pct": 32 },
    "ramp": { "in_warmup": true, "sent": 180, "cap": 500, "remaining": 320, "ends_at": "2026-08-04T13:00:00Z" }
  }
}

New campaigns warm up after approval — a capped send limit for the first couple of business days. ramp.in_warmup is false once you're past it; while true, pace within ramp.remaining to avoid warm-up rejections.

PUT/v1/registration/:id/nameRename a brand / campaign

Body: { name }. Display-name only — doesn't affect carrier status.

DELETE/v1/registration/:idCancel a registration

Cancels the registration and stops its subscription. An approved registration can't be deleted — contact support.

Billing

Query credit balance and transaction history.

GET/billing/balanceCredit balance and usage

Returns current balance and usage summary for the account.

GET/billing/transactionsTransaction history

Paginated credit transactions (top-ups and usage).

Webhooks

Ready POSTs events to a URL you own as they happen — new replies, deliveries, opt-outs and more. Configure it in Settings → Integrations → Webhooks, or entirely over the API:

PUT/v1/webhooksSet your callback URL + events

Body

urlstringYour https:// endpoint. required
eventsstring[]Any of inbound_reply, message_delivered, message_failed, contact_opted_out. Omit for all.

Response 200

JSONCopy
{ "success": true, "data": { "url": "https://you.com/hook", "events": ["inbound_reply","message_failed"], "enabled": true } }
GET/v1/webhooksRead your webhook config

Returns your current url, subscribed events, and enabled flag.

No code required to receive them — you just need a URL that accepts an HTTP POST.

Events

message_deliveredmessage_failedinbound_replycontact_createdcontact_opted_out

Payload

Every webhook is a JSON POST shaped { event, timestamp, data }:

inbound_reply payloadCopy
{
  "event": "inbound_reply",
  "timestamp": "2026-07-25T14:30:00.000Z",
  "data": {
    "message_id": 12345,
    "contact_id": 101,
    "from": "+15551234567",
    "to": "+15559876543",
    "body": "Yes, I'm interested!",
    "segments": 1
  }
}

Verifying the signature

Every request carries an X-Signature: sha256=<hash> header — the HMAC-SHA256 of the exact raw request body, keyed with your webhook signing secret (shown in Settings → Integrations). On your endpoint, recompute hex(hmac_sha256(secret, rawBody)) and constant-time compare it to the value after sha256=; if they match, the request genuinely came from Ready. Treat the secret like a password — regenerating it takes effect immediately, so update your receiver at the same time.

verify (Node.js)Copy
const crypto = require('crypto');
const expected = 'sha256=' + crypto.createHmac('sha256', SECRET)
  .update(rawBody).digest('hex');
const ok = crypto.timingSafeEqual(
  Buffer.from(req.headers['x-signature'] || ''),
  Buffer.from(expected)
);

Using with n8n or Make.com

No server needed. In n8n or Make.com, add a Webhook trigger — it gives you a URL. Paste that URL into Ready's Webhook URL field, pick your events, and Save. Ready then POSTs every matching event into your scenario, where you can route it into thousands of apps with no code.

Zapier

Ready has a published Zapier app — connect 2,000+ apps with no code. Add Ready in Zapier, authorize with OAuth, then build Zaps:

  • Triggers — new message, new contact, new conversation, opt-out, delivery status, pipeline changes.
  • Actions — send SMS, create/update a contact, add/remove tags, enroll in a drip.
  • Searches — find a contact by phone or email.

Under the hood Zapier uses Ready's dedicated OAuth + webhook surface (/zapier/oauth/authorize, /zapier/oauth/token, /zapier/oauth/test, plus subscribe/unsubscribe hooks) — you rarely call these directly.

MCP (AI agents)

Ready runs a full MCP (Model Context Protocol) server so AI agents — Claude, Cursor, Windsurf, ChatGPT, and ClickUp — can operate your account in natural language: send SMS, manage contacts and conversations, build and send campaigns, enroll drips, check number deliverability, and read billing. It exposes roughly 60 tools.

Endpoint

MCP serverCopy
https://api.tryready.com/mcp

Streamable HTTP with JSON-RPC 2.0 — POST /mcp for calls, GET /mcp for the SSE stream; sessions are tracked via the Mcp-Session-Id header.

Connecting

Authenticate with OAuth 2.0 (PKCE — your client walks you through a Ready sign-in) or pass an rsms_ API key as a bearer token (see Authentication). In an MCP-aware client, add Ready as a remote server:

mcp configCopy
{
  "mcpServers": {
    "ready": {
      "url": "https://api.tryready.com/mcp",
      "headers": { "Authorization": "Bearer rsms_your_api_key_here" }
    }
  }
}

Clients that support OAuth discovery can connect with no key — they'll be prompted to sign in. Recognized clients include Claude, Cursor, Windsurf, ChatGPT, and ClickUp.

Prefer no code? The Integration Assistant can issue API keys, subscribe webhooks, and send a test message for you.