Developer documentation

Developer platform documentation · Updated 21 July 2026 · View changelog →

Build better customer communications with one developer platform

Send SMS and RCS messages, manage contacts, receive replies and track interactions through the QuickSMS developer platform.

Copies a machine-readable version of the documentation for use with AI coding assistants, also available at llms.txt.

One platform, every channel

QuickSMS is one developer platform with two current outbound messaging APIs: use the Messaging API (POST /v1/messages) for SMS and RCS text, and the Rich RCS API (POST /v2/messages) for cards, carousels, media and interactions. Each has its own request shape, status vocabulary and delivery receipt, documented side by side; credentials, sending controls and webhook signing are shared. Messaging and the Contact Book use separate credentials today, explained once in Authentication.

Choose a channel

SMS and RCS text are sent through the Messaging API; Rich RCS is its own opt-in API with a structured content model and its own delivery receipts. Pick a channel once and the messaging pages show that channel's complete journey, see choose a messaging channel.

Capability SMS RCS Text Rich RCS
Text content Yes Yes Yes
Branded sender (name, logo, colour) No Yes Yes
Images & media No No Yes
Buttons & suggested actions No No Yes
Rich cards & carousels No No Yes
Delivery receipts Yes Yes Yes
Read receipts No Yes Yes
SMS fallback None Automatic You control it

Not sure where to start? Send an SMS first, it works for every recipient with no setup beyond an approved sender ID. Once you have an approved RCS agent, add smart_routing to deliver via RCS text where the handset supports it, then move to rich cards and carousels when you're ready.

How the platform fits together

  1. Contacts are your audience layer, who you can message, how they're organised, and what they've consented to.
  2. Messaging sends to a recipient over the channel you choose, with content the channel supports.
  3. Events tell you what happened next, delivered, read, failed, or fell back to SMS, with the payload for each endpoint clearly documented.
  4. Errors use consistent envelope principles, with API-specific rejection and delivery models documented separately.

Messaging & Contact Book credentials Updated 21 July 2026 · View changelog →

Authentication

Every QuickSMS API request is authenticated with a Bearer credential in the Authorization header. This page is the single source of truth for credentials, the rest of the documentation links here rather than repeating it.

All APIs use the same header format, over HTTPS only:

Header
Authorization: Bearer sk_test_your_key_here

Credentials are secrets. They're shown once at creation, store them in a secrets vault or environment variable, never in source control. If a credential leaks, revoke it in the portal and create a new one.

Which credential do I need?

Today, QuickSMS capabilities are covered by two credential types, both created in this portal. Messaging (SMS, RCS text and Rich RCS) shares one API key; the Contact Book has its own token with fine-grained scopes.

Capability Requirements Credential prefix Create or configure in
SMS Messaging API key + approved SMS sender ID sk_live_ / sk_test_ API Connections · Sender IDs
RCS Text Messaging API key + approved RCS agent + SMS fallback sender sk_live_ / sk_test_ API Connections · RCS Agents
Rich RCS Messaging API key + approved RCS agent (while the agent is in the test phase, the receiving handset must be registered as a tester); fallback sender where SMS fallback is enabled sk_live_ / sk_test_ API Connections · RCS Agents
Contact Book (contacts, lists, tags, opt-outs, custom fields) Contact Book token cbk_ / cbk_test_ Contact Book › API Integration
Inbound V2 Configured inbound number and at least one routing destination Per-number webhook signing secret Management › Numbers
Existing RCS inbound Existing Inbound V1 configuration (deprecated, retained for existing integrations) Existing V1 model Your existing integration

The two credential types are architecturally separate: a Contact Book token cannot send messages, and a messaging key cannot read your contact book. This limits the blast radius of a leaked credential and lets you grant integrators exactly what they need. If your integration does both, create both.

Test and live environments

Both credential types come in a test and a live variant. The base URL never changes, the credential prefix selects the environment. Use the Test / Live switch in the sidebar to update every example on these pages. How the two environments differ for messaging:

BehaviourTest (sk_test_)Live (sk_live_)
Handset deliverySimulated, nothing is sentReal
CreditsNot chargedCharged per message part
RecipientMust be in your approved test allowlist, otherwise TEST_NUMBER_NOT_APPROVEDAny permitted destination
SenderThe default QuickSMS test sender, unless you have an approved registered sender or purchased numberYour approved registered sender
Appended wording"This is a QuickSMS test message" is appendedNone
Anti-floodBypassedApplied
Out-of-hoursBypassedApplied
Delivery receiptsSimulated after 3 to 5 secondsNetwork-derived
RCS read receiptsSimulated after 5 to 10 seconds (RCS only)From the recipient's device (RCS only)

Manage the test allowlist on the API connection in API Connections. A live key can opt into the same behaviour per request with "sandbox": true, see sandbox mode. The Contact Book test token (cbk_test_) works differently: it is a real API against an isolated data namespace.

Simulated outcomes in test mode

Simulated delivery receipts arrive a few seconds after the send. You can force specific outcomes with the recipient's mobile number ending; read receipts are simulated only for RCS-delivered messages, SMS never produces read:

Mobile number ends inSimulated outcome
0000UNDELIVERED with GSM code 27, absent subscriber (Rich RCS payloads report failed)
9999EXPIRED with GSM code 255 (Rich RCS payloads report expired)
Anything elseDELIVERED; RCS-delivered messages then report a read receipt

Use these endings to exercise your webhook handler against every delivery outcome.

Two different "test" concepts, don't confuse them. The QuickSMS API sandbox (sk_test_ keys, above) simulates the entire flow and never delivers to a handset. Google's RCS-agent test phase is separate: while your RCS agent is being verified, live RCS (sk_live_) can only reach handsets you have registered as testers on the agent. So a tester handset matters only for real RCS sends during agent verification, not for sandbox sends, which reach no handset at all. A sandbox sk_test_ send never requires a tester handset.

The Contact Book sandbox is a separate data namespace: create, import and delete freely, then wipe it with POST /v1/sandbox/reset. Sandbox responses carry an X-Sandbox-Mode: true header so your client can display a test-mode indicator. Don't store real personal data in the sandbox.

Scopes (Contact Book)

Contact Book tokens carry scopes chosen at creation. Give each integration the narrowest scopes that work, a read-only dashboard needs contacts:read and nothing else.

ResourceRead scopeWrite scope
Contactscontacts:readcontacts:write
Tagstags:readtags:write
Listslists:readlists:write
Opt-outsopt-outs:readopt-outs:write
Custom fieldscustom-fields:readcustom-fields:write

Wildcards are supported: contacts:* grants all contact endpoints, * grants everything. A request without the required scope returns 403 with the missing scope and your token's actual scopes echoed back, see Errors.

Credential lifecycle

The two credential families have different lifecycles today. They are shown separately here because the states, and what an IP restriction failure returns, differ between them.

Messaging API connections

StateBehaviour
DraftCreated but not yet live; requests are rejected until the connection is activated.
ActiveWorks normally.
SuspendedRequests are rejected until the connection is reactivated.
ArchivedRetired permanently; create a new connection.

A request from an IP outside a messaging connection's allowlist returns 403 with the code IP_NOT_ALLOWED.

Contact Book tokens

StateBehaviourReversible?
ActiveWorks normally.Not applicable
SuspendedReturns 401 Unauthorized. Pause access without losing the token.Yes, reactivate any time.
RevokedReturns 401 Unauthorized permanently.No, create a new token.
ExpiredIf you set an expiry date, the token stops working after it.No, create a new token.

A request from an IP outside a Contact Book token's allowlist returns 401 Unauthorized. For both families, leave the allowlist empty to accept requests from anywhere.

Basic authentication (compatibility)

The messaging API also accepts HTTP Basic authentication for compatibility with older integrations; Bearer is recommended for anything new. Your API key is the username and the password is blank, so the value is the Base64 encoding of the key followed by a trailing colon:

Basic authentication header
Authorization: Basic base64(sk_test_your_key_here:)

The trailing colon is required; the password field after it is ignored.

Brute-force protection

After 10 consecutive failed authentication attempts from the same IP within 60 seconds, that IP is locked out for 5 minutes. Locked-out requests return 429 with the code AUTH_RATE_LIMITED and a Retry-After header. Wait the indicated interval and verify the key is correct rather than retrying in a tight loop.

Best practices

  • One credential per integration, easier to audit and revoke individually.
  • Scope down, grant the minimum capability each integration needs.
  • Rotate periodically, create the new credential, switch over, then revoke the old one.
  • Separate test from live, CI and staging should only ever hold test credentials.

Verify your credential

Before integrating against real resources, confirm your credential works:

/v1/ping requires no scopes and echoes back your token's name, scopes and access level. For the messaging API, the health endpoint (GET /v1/health, also at /v2/health and /health) reports service status without authentication, and a test-key send to an allowlisted test number exercises the full authenticated flow.

Messaging API Updated 21 July 2026 · View changelog →

Send your first message

From API key to delivery receipt in seven steps. Everything here runs safely against a test key, nothing is sent to a real handset and no credits are used. Your progress is saved in this browser.

Compare messaging channels →

The steps are the same for every channel; the request, response and prerequisites below follow your selection. Shared steps stay complete when you switch channel; the send and response steps track completion per channel.

Before you start (SMS), you need a portal login with permission to manage API connections and an approved test recipient on your allowlist. The default QuickSMS test sender is used until you have an approved sender ID.

Before you start (RCS text), you need a portal login with permission to manage API connections, an approved RCS agent, an approved test recipient on your allowlist, and an approved SMS fallback sender.

Before you start (Rich RCS), you need a portal login with permission to manage API connections and a test or approved RCS agent. While your agent is in Google's test phase, the receiving handset must be registered as a tester on the agent.

1

Create an API key

Not started

Go to Management › API Connections and create a new connection. Copy the key when it's shown, it's displayed once. You'll get a test key (sk_test_…) and can enable a live key (sk_live_…) when you're ready.

Credentials, scopes and environments are covered in depth in Authentication.

2

Choose your environment

Not started

Stay on Test for this guide (check the switch in the sidebar, it updates every code example on this page). The base URL is the same in both environments; your key prefix selects the environment:

Base URL
https://api.quicksms.com

Test keys simulate the entire flow, including delivery statuses and webhooks. In test mode a recipient number ending 0000 simulates UNDELIVERED and 9999 simulates EXPIRED, anything else is delivered.

3

Send a message

Not started

One request to the Messaging API sends your message with smart routing: RCS where the handset supports it, SMS otherwise. You need an approved RCS agent as the sender, and an approved SMS sender for fallbacks.

One request to the Rich RCS API sends a branded card. You need an approved RCS agent; note the different endpoint and the mobile_number field name on this API.

One request to the Messaging API sends your message. MSISDN is the recipient's mobile number in international format, digits only, country code first, no + (for example 447700900123).

Replace YOUR_SENDER_ID with an approved sender from Sender ID Registration. In test mode the default QuickSMS test sender applies until you have one, and recipients must be on your test allowlist.

4

Review the response

Not started
202 Accepted
{
  "sandbox": true,
  "total": 1,
  "accepted": 1,
  "rejected": 0,
  "messages": [{
    "quicksms_id": "msg_42a731415e521124",
    "mobile_number": "447700900123",
    "MSISDN": "447700900123",
    "sender": "YOUR_SENDER_ID",
    "status": "ACCEPTED",
    "channel": "SMS",
    "encoding": "GSM-7",
    "message_parts": 1
  }],
  "created_at": "2026-07-15T10:10:59+00:00"
}

Store the quicksms_id, it identifies this message in the delivery receipt. status: "ACCEPTED" means the message was validated and accepted for dispatch, and message_parts is what you are billed: one segment here, more for longer or Unicode texts, see encoding and billing.

202 Accepted
{
  "sandbox": true,
  "total": 1,
  "accepted": 1,
  "rejected": 0,
  "messages": [{
    "quicksms_id": "msg_42a731415e521124",
    "mobile_number": "447700900123",
    "MSISDN": "447700900123",
    "sender": "YOUR_RCS_AGENT",
    "status": "ACCEPTED",
    "channel": "RCS",
    "encoding": null,
    "message_parts": 1
  }],
  "created_at": "2026-07-15T10:10:59+00:00"
}

Store the quicksms_id. channel: "RCS" is the accepted route, not a guarantee: the delivery receipt reports the channel actually used, and a fallback to SMS is re-rated per message part, see how smart routing works.

202 Accepted
{
  "total": 1,
  "accepted": 1,
  "rejected": 0,
  "messages": [{
    "message_id": "msg_42a731415e521124",
    "to": "447700900123",
    "sender": "YOUR_RCS_AGENT",
    "status": "queued",
    "channel": "RCS",
    "content_type": "rich_card",
    "cost": 0.10
  }],
  "created_at": "2026-07-15T10:10:59+00:00"
}

Store the message_id: on the Rich RCS API it identifies the message in every message.status webhook and in GET /v2/messages/{message_id}. The cost is provisional if the card falls back to SMS; example values are illustrative.

Note the field names differ from the Messaging API (message_id not quicksms_id, to not MSISDN, lowercase statuses). If you use both APIs, see the field-name mapping.

5

Track delivery

Not started

Pass a callback_url on the send (or configure one on your API connection) and QuickSMS POSTs you a signed delivery receipt (DLR) when the message reaches a final status: uppercase DELIVERED, or UNDELIVERED / EXPIRED / REJECTED with a GSM status_code and the billed message_parts. In test mode the simulated receipt arrives within a few seconds. There is no polling endpoint; verify the signature before trusting the receipt, see SMS delivery receipts.

You receive the same Messaging API delivery receipt (DLR), with the channel field reporting the route actually used. RCS-delivered messages add read_receipt: true; messages that fell back to SMS carry the GSM status_code and message_parts, exactly like SMS. In test mode the simulated receipt arrives within a few seconds. Verify the signature first, see RCS text delivery receipts.

Rich RCS sends emit a message.status webhook on each transition, using the lowercase vocabulary (queuedsubmitteddeliveredread, or failed / expired / fallback_delivered) with a failure.reason on failures. Store the message_id and reconcile any time with GET /v2/messages/{message_id}. Verify the signature first, see Rich RCS delivery receipts.

6

Receive a reply

Not started

Send from an inbound number (not an alphanumeric sender) and SMS replies flow back through the routing you enable on that number: the portal Inbox, email forwarding, and an HMAC-signed Inbound V2 webhook to your endpoint. SMS inbound is available through Inbound V2 today.

RCS text replies behave like SMS: send from an inbound number and replies arrive through your Inbound V2 routing. Replies to your RCS agent are delivered through RCS inbound, configured per Live agent. Existing Inbound V1 integrations keep working but are deprecated; plan the migration.

Rich RCS interactions are delivered through RCS inbound, configured per Live agent: typed replies, reply chip taps (with your payload echoed back), button taps, photos and files, and share_location coordinates each have a worked example. Buttons like open_url and dial act on the handset and also report the tap as message_type: "suggested_action".

7

Move to production

Not started

Before switching to your sk_live_ key, check the items for your channel:

  • Your SMS sender ID is approved (register here); live sends require it.
  • Your webhook endpoint verifies signatures and responds quickly, see signature verification.
  • You handle the error model, including 429 backoff, see Errors & rate limits.
  • You send an Idempotency-Key on writes so retries are safe.
  • You have credits and respect recipient opt-outs, see opt-out management.
  • Your RCS agent is approved (register here); smart routing requires it.
  • Your SMS fallback sender is approved so fallbacks stay on-brand, see the fallback sender order.
  • Your webhook endpoint verifies signatures, see signature verification.
  • You handle the error model and send an Idempotency-Key on writes.
  • You have credits and respect opt-outs.
  • Your RCS agent is approved for production (register here); Google verification typically takes 1 to 2 weeks. While the agent is in the test phase, only handsets registered as testers receive live RCS.
  • You provide an explicit SMS fallback with an approved fallback sender for non-RCS recipients.
  • Your webhook endpoint verifies signatures and handles the message.status payload, see signature verification.
  • You handle the error model, send an Idempotency-Key, and respect opt-outs.

That's the whole loop: send, track, reply, go live, on the channel you chose. Switch the selector at the top to walk the same loop for another channel.

Messaging API Current · Live · Fully supported· Updated 21 July 2026 · View changelog →

Messaging

The QuickSMS Messaging API sends SMS and RCS text through one endpoint, POST /v1/messages, with one request shape and one status vocabulary. Rich content (cards, carousels, media) is an opt-in capability on an additional endpoint, POST /v2/messages, covered in Rich RCS; nothing about your existing integration changes when it is adopted.

The message model

Every message you send has the same anatomy:

POST /v1/messages · request shape
{
  "mobile_number": "447700900123",                  // who, the recipient's mobile number
  "sender": "QuickSMS",                      // from, sender ID or RCS agent
  "msg": "Your order has been dispatched.",  // what, the message text
  "smart_routing": false,                    // RCS with automatic SMS fallback
  "expiry": 172800,                          // how long delivery may be attempted (seconds)
  "customer_id": "ORDER-12345",              // your reference, echoed in webhooks
  "metadata": { "journey": "dispatch" },     // your key-value data, echoed in webhooks
  "callback_url": "https://example.com/hooks"// where delivery receipts are sent
}
FieldRequiredDescription
mobile_number Required The recipient's mobile number in international format, using digits only. Include the country code and omit the leading +, spaces and punctuation. Example: 447700900123. MSISDN is accepted as a fully supported alias for existing integrations (see the note below). The Rich RCS endpoint uses the same mobile_number field; the Contact Book API differs, storing E.164 with a leading +.
msg Required The message text, 1 to 1,530 characters. This field's shape is stable and will not change. If you are starting a new integration and want the unified structured content model, the Rich RCS endpoint (POST /v2/messages) also accepts plain text; /v1/messages remains fully supported and unchanged.
sender Required SMS: an approved SMS sender ID. RCS text (smart_routing): your approved RCS agent name (max 25 chars).
smart_routing Optional Deliver via RCS where the handset supports it, SMS otherwise. Default false. Requires an approved RCS agent as the sender.
sms_failover_sender Optional SMS sender ID for messages that fall back from RCS to SMS, see the resolution order.
expiry Optional Validity in seconds. Min 60, max 259,200 (72 h). Default 172,800 (48 h). After this the message becomes EXPIRED.
customer_id Optional Your own reference, max 128 chars. Returned in every delivery receipt.
metadata Optional Key-value pairs (max 10 keys, values max 256 chars). Returned in every delivery receipt.
callback_url Optional Per-message webhook URL (max 2,048 chars). Overrides the connection default.
sandbox Optional Force test behaviour on a live key. Default false.

The full request/response reference, including the uppercase status vocabulary and the delivery receipt payload, is on the Messaging API reference.

Recipient field: mobile_number (preferred) or MSISDN. To converge naming across the platform, POST /v1/messages accepts mobile_number as the recipient field. New integrations should use it. The original MSISDN field remains fully supported for existing integrations, indefinitely, with no deprecation. Send one or the other; if both are present with different values the request is rejected with a validation error.

Requests also accept an Idempotency-Key header (cached 24 hours) so a retried send never delivers twice, see Idempotency.

Sender formats and validation

Sender typeRulesExampleIf invalid
Alphanumeric sender ID Max 11 characters, must contain at least one letter. Recipients cannot reply. Must be registered and approved before use. MyStore SENDER_NOT_REGISTERED
Numeric sender (inbound number) Max 15 digits. Use an inbound number when you expect replies. 447700900123 SENDER_NOT_REGISTERED
RCS agent Max 25 characters. Required whenever smart_routing is enabled or the content is rich. MyBrandAgent SENDER_NOT_RCS_AGENT

Sender availability is also country-specific: the sender must be approved for the destination you are sending to. In test mode the default QuickSMS test sender applies unless you have an approved sender, see test and live environments.

Content length limits

FieldLimit
SMS text (msg; content.text on the Rich RCS endpoint)1 to 1,530 characters. Longer is rejected with CONTENT_TOO_LONG.
SMS fallback body (fallback.sms_body)1 to 1,530 characters.
Rich card titleMax 200 characters.
Rich card descriptionMax 2,000 characters.
Button and suggestion labelsMax 25 characters.

Send a message

Pick a channel once; the selection follows you across the messaging pages (and into shared links) so you see one complete, channel-specific journey.

Compare messaging channels →

SMS Plain text to any mobile, worldwide. No recipient requirements, no agent registration, the universal baseline. Long messages are split into segments and billed per segment.

Before you start: an approved sender ID (in test mode the default QuickSMS test sender applies until you have one).

Response · 202 Accepted
{
  "total": 1, "accepted": 1, "rejected": 0,
  "messages": [{
    "quicksms_id": "msg_9f2b60c1d8a4e7b3",
    "mobile_number": "447700900123",
    "MSISDN": "447700900123",
    "sender": "YOUR_SENDER_ID",
    "status": "ACCEPTED",
    "channel": "SMS",
    "encoding": "GSM-7",
    "message_parts": 1
  }],
  "created_at": "2026-07-15T09:30:00+00:00"
}

What happens next

A delivery receipt follows when the message reaches a final status: DELIVERED, or UNDELIVERED / EXPIRED / REJECTED with a GSM status code. SMS has no read receipts, DELIVERED is the terminal success state.

Limitations

  • Text only, no media, buttons or branding.
  • 160 GSM characters per segment (70 for Unicode); longer texts are concatenated and billed per segment.

RCS Text The same text content, upgraded in transit: with smart_routing enabled, recipients on RCS-capable handsets receive the message in their native RCS inbox, with read receipts, and everyone else receives a standard SMS automatically. One request, best available delivery.

Before you start: you need an approved RCS agent for RCS text delivery. You should also configure an approved SMS sender ID for messages that fall back to SMS; the fallback sender is resolved through a defined priority order, so set an account default sender to keep fallbacks on-brand. See how smart routing works for the full decision flow, channel reporting and billing.

Response · 202 Accepted
{
  "total": 1, "accepted": 1, "rejected": 0,
  "messages": [{
    "quicksms_id": "msg_5c11a09eb7f2d846",
    "mobile_number": "447700900123",
    "MSISDN": "447700900123",
    "sender": "YOUR_RCS_AGENT",
    "status": "ACCEPTED",
    "channel": "RCS",
    "encoding": null,
    "message_parts": 1
  }],
  "created_at": "2026-07-15T09:30:00+00:00"
}

What happens next

The delivery receipt reports the channel actually used. RCS-delivered messages can also report a read: the receipt carries read_receipt: true and read_receipt_timestamp. If the message is delivered as SMS instead, it behaves exactly like the SMS channel, the receipt shows channel: "SMS" with a GSM status code, and the send is billed on the SMS rendition's parts rather than as RCS text.

Billing

RCS text is billed by length: up to 160 characters as RCS Basic, 161 or more as RCS Single. If the message falls back to SMS, the SMS rendition is billed independently on its own encoding and parts.

Limitations

  • Text only; for rich content (images, buttons, cards) see the Rich RCS tab.
  • Read receipts arrive only for RCS-delivered messages, and only where the recipient's app reports them.

Rich RCS Branded cards, carousels, images and interactive buttons, sent as your verified RCS agent, your brand name, logo and colour in the recipient's native inbox. You control exactly what non-RCS recipients receive through the explicit fallback object. Rich RCS lives on an additional, opt-in endpoint, POST /v2/messages, with its own request shape (mobile_number and a structured content object) and lowercase status payloads; your existing sends are unaffected. Full guide: Rich RCS.

Before you start: an approved RCS agent on your account. Registration includes verification with Google and typically takes 1 to 2 weeks; while the agent is in the test phase your handset must be added as a tester.

Response
{
  "total": 1, "accepted": 1, "rejected": 0,
  "messages": [{
    "message_id": "msg_42a731415e521124",
    "to": "447700900123",
    "sender": "MyBrandAgent",
    "status": "queued",
    "channel": "RCS",
    "content_type": "rich_card",
    "fallback": { "enabled": true, "sender_id": "MyBrand" },
    "cost": 0.10
  }],
  "created_at": "2026-07-14T09:30:00+00:00"
}

No silent downgrade. If content.type is anything other than text and sender is not an approved RCS agent, the request is rejected synchronously with SENDER_NOT_RCS_AGENT. Rich content never quietly converts to SMS at send time, fallback only happens when RCS delivery to the handset is not possible.

What happens next

Delivered rich messages can progress to read. If the handset can't receive RCS and fallback is enabled, your sms_body is delivered as SMS and the terminal status is fallback_delivered, with fell_back_to_sms: true in webhooks and retrieval.

Limitations

  • Rich content requires the recipient to be RCS-capable, always design the SMS fallback alongside the card.
  • Media URLs must be HTTPS and publicly reachable, see media requirements.
  • Rich cards and carousels are billed as RCS Single; any SMS fallback is billed separately on its own parts.

Build a request

Construct a working request without writing JSON by hand. Fill in the fields and copy the generated request in any of the eight languages; nothing is sent from this page, and the example key follows the Test / Live switch in the sidebar. Validation problems are flagged before you copy.

How RCS is billed

QuickSMS UK RCS billing classification. RCS Basic and RCS Single are QuickSMS commercial billing products for UK RCS, not Google message classifications. The message content and format determine which product applies. The 160-character boundary and the character-counting rule are QuickSMS production billing behaviour, not a universal property of RCS.

Message createdRCS billing classificationPotential SMS fallback
Text, 160 characters or fewerRCS BasicCalculated using SMS encoding and message parts
Text, 161 characters or moreRCS SingleCalculated using SMS encoding and message parts
Rich cardRCS SingleCalculated using the fallback SMS text, encoding and message parts
CarouselRCS SingleCalculated using the fallback SMS text, encoding and message parts
Standalone media (image or file, no card)RCS SingleCalculated using the fallback SMS text, encoding and message parts
Text with suggested repliesRCS Basic to 160 characters, RCS Single 161 or moreCalculated using SMS encoding and message parts
Text with suggested actionsRCS SingleCalculated using SMS encoding and message parts

Full explanation: RCS billing classification.

Next step: delivery receipts for this channel. The generated snippets are quick-start examples; for the production pattern (timeouts, non-2xx handling, 429 and Retry-After, idempotency-key reuse, request_id logging) see retrying API requests.

How smart routing works

Smart routing is a QuickSMS orchestration capability, not a Google feature: QuickSMS decides between RCS and SMS for a text message and applies your configured SMS fallback when the recipient or route is unavailable. It is implemented through our gateway's failover configuration. The whole flow, from request to billing:

Routing decision
Text request with "smart_routing": true
        |
        v
Is the sender an approved RCS agent?
        |-- no --> rejected synchronously (SENDER_NOT_RCS_AGENT), nothing sent
        |-- yes
        v
Is the recipient's handset RCS-capable and reachable?
        |-- yes --> delivered as RCS, read receipts possible
        |-- no  --> delivered as SMS via your fallback sender
  • No silent downgrade to guesswork: if the sender isn't an approved RCS agent, the request is rejected synchronously; it never quietly sends as something else.
  • The delivered channel is reported, not assumed: the send response reports the accepted channel, but the actual channel is confirmed by the delivery receipt's channel field (Rich RCS payloads additionally carry fell_back_to_sms: true, and support GET /v2/messages/{id}). An RCS-accepted message can still deliver, and bill, as SMS.
  • Billing follows the delivered channel: RCS text is billed by length, up to 160 characters as RCS Basic, 161 or more as RCS Single; SMS is billed per message part. When a message falls back to SMS, the SMS rendition is calculated and billed independently on its own encoding and parts.
  • Read receipts are RCS-only: read_receipt / read_at are populated only for RCS-delivered messages and are null for SMS.

How the SMS fallback sender is chosen

When a message falls back to SMS, the sender ID for the SMS is resolved in this order:

PrioritySourceDescription
1 (highest)Explicit fallback sender on the requestSmart routing: sms_failover_sender. Rich content: fallback.sender_id.
2Your account's default senderThe registered sender marked as default.
3First approved senderThe first approved sender on your account.
4 (last resort)Sanitised agent nameThe RCS agent name stripped of non-alphanumeric characters and truncated to 11 characters.

Always control your fallback sender. Set a default sender on your account or pass the explicit fallback sender; the last-resort sanitised agent name may not match your brand. The fallback sender must itself be approved and valid for the destination country.

How the delivered channel is billed

Billing follows the channel the message was actually delivered on. An RCS text message is billed by length (RCS Basic or RCS Single); a message that falls back to SMS is billed on the SMS rendition's encoding and parts, calculated independently. The delivery receipt and, for Rich RCS, GET /v2/messages/{id} report the final channel and, on SMS delivery, the message_parts. Amounts are in your account currency; the exact reservation and re-rating behaviour on your wallet is described in your commercial terms.

Worked example · RCS text that falls back to a 2-part SMS
// A 170-character RCS text send, accepted as RCS.
// RCS delivery would bill as RCS Single (161+ characters).

// Recipient is not RCS-capable; the same text is delivered as SMS instead.
// Delivery receipt: final channel is SMS, billed on its own parts.
{ "quicksms_id": "msg_42a731415e521124",
  "message_status": "DELIVERED",
  "channel": "SMS", "status_code": 0,
  "message_parts": 2 }
// Delivered as SMS: billed as two GSM-7 SMS parts, calculated
// independently from the RCS classification.

Scheduling and expiry

expiry caps how long delivery may be attempted (60 seconds to 72 hours, in seconds: min 60, max 259,200, default 172,800). If the recipient's handset stays unreachable past the validity period, the message ends as expired and you receive a failure receipt. Future-dated scheduling (send_at) is Coming soon, today, schedule sends from your own system or through portal campaigns.

Delivery is only attempted for five minutes; after that the code is useless anyway, so the message expires instead of arriving late.

Encoding, message parts and billing

SMS text is encoded as GSM-7 where possible: 160 characters in a single message, 153 per segment when concatenated (a few GSM-7 extension characters, such as , [ and ~, count as two). Any character outside the GSM-7 set (many emoji, some accented characters, smart quotes pasted from a document) switches the whole message to UCS-2: 70 characters single, 67 per segment concatenated. SMS billing is per message part, and the maximum accepted length is 1,530 characters. This SMS segment counting applies to plain SMS and to any RCS message that falls back to SMS.

RCS billing classification

RCS text-message billing is determined by message length; rich formats have their own classification. The boundary between RCS Basic and RCS Single is a plain character count, so Unicode characters and emoji do not move it (they affect only an SMS fallback's encoding and parts).

QuickSMS UK RCS billing classification. RCS Basic and RCS Single are QuickSMS commercial billing products for UK RCS, not Google message classifications. The message content and format determine which product applies. The 160-character boundary and the character-counting rule are QuickSMS production billing behaviour, not a universal property of RCS.

Message createdRCS billing classificationPotential SMS fallback
Text, 160 characters or fewerRCS BasicCalculated using SMS encoding and message parts
Text, 161 characters or moreRCS SingleCalculated using SMS encoding and message parts
Rich cardRCS SingleCalculated using the fallback SMS text, encoding and message parts
CarouselRCS SingleCalculated using the fallback SMS text, encoding and message parts
Standalone media (image or file, no card)RCS SingleCalculated using the fallback SMS text, encoding and message parts
Text with suggested repliesRCS Basic to 160 characters, RCS Single 161 or moreCalculated using SMS encoding and message parts
Text with suggested actionsRCS SingleCalculated using SMS encoding and message parts
  • A plain RCS text message containing up to 160 characters is billed as RCS Basic.
  • A plain RCS text message containing 161 characters or more is billed as RCS Single.
  • A rich card is billed as RCS Single. A carousel is billed as RCS Single. Standalone media (an image or file sent without a card) is billed as RCS Single.
  • Adding suggested replies to a text message does not change its classification: it stays RCS Basic to 160 characters and RCS Single at 161 or more.
  • Adding a suggested action (open URL, dial, view location, create calendar event, or open in webview) classifies the message as RCS Single, regardless of length.
  • A text message over 160 characters remains an RCS text message. It does not become a rich card or carousel, and being billed as RCS Single does not make it Rich RCS.
  • Potential SMS fallback is calculated separately from the RCS classification: it depends on the fallback text, its SMS encoding and the number of SMS parts.

If the message cannot be delivered through RCS and falls back to SMS, the SMS rendition is calculated and billed independently according to its encoding and number of SMS message parts. The two calculations are separate: the request builder shows both at once.

Message lengthEncodingBillable parts
160 charactersGSM-71
161 charactersGSM-72
306 charactersGSM-72
307 charactersGSM-73
70 charactersUCS-21
71 charactersUCS-22
134 charactersUCS-22
135 charactersUCS-23

Above one segment the formula is ⌈length ÷ 153⌉ for GSM-7 and ⌈length ÷ 67⌉ for UCS-2. The response tells you what you were billed:

A 200-character GSM-7 message
{
  "quicksms_id": "msg_42a731415e521124",
  "status": "ACCEPTED",
  "channel": "SMS",
  "encoding": "GSM-7",
  "message_parts": 2
}
// This SMS contains two message parts and is billed as two SMS messages.

The same message, with and without one emoji. This text is exactly 150 GSM-7 characters:

150 characters · GSM-7 · 1 part
Hi Alex, your table at The Rose & Crown is booked for 7pm tonight. Reply C to confirm, or call us on 020 7946 0000 if you need to change your booking.
TextLengthEncodingPartsBilled
The message above, exactly as written150 charactersGSM-711 × SMS rate
The same message with " 🎉" appended153 characters (the space is 1; an emoji counts as 2 in UCS-2)UCS-23 (153 ÷ 67, rounded up)3 × SMS rate

Paste the text into the request builder above and watch the encoding flip as you add the emoji.

One emoji turned one billable message into three. Check your message before sending: use the QuickSMS SMS & RCS character counter to see the encoding, number of message parts and likely charge as you type.

Personalisation and references

Render personalised text in your application before sending, you know your data best; QuickSMS does not substitute metadata into the message text. Use customer_id for your primary reference and metadata for structured hidden context (an order ID, a journey step). Both are echoed back on retrieval and on every delivery receipt, so your handler can correlate without a database lookup. To personalise from stored QuickSMS contact data, see Contacts & Audiences.

  • customer_id: one string, max 128 characters.
  • metadata: flat key-value pairs, max 10 keys, each value max 256 characters; no nested objects.
  • Metadata is stored and echoed back to you, so don't put sensitive personal data in it unless you would be comfortable seeing it in your own webhook logs.

Sending in bulk

The send response is shaped for multiple recipients (total/accepted/rejected with a messages array). Batch sending of up to 1,000 recipients per request is Coming soon; today, send per recipient (respecting rate limits) or run large campaigns from the portal against your contact lists.

Rich RCS API Current · Live · Fully supported· Updated 21 July 2026 · View changelog →

Rich RCS

Branded cards, carousels, images and interactive buttons, delivered in the recipient's native inbox through the same POST /v2/messages endpoint as everything else. This page shows what each content type looks like on the handset, beside the JSON that creates it.

Overview

Rich content is sent as your RCS agent: a verified sender with your brand name, logo and colour. Register an agent before your first rich send; registration includes verification with Google and typically takes 1 to 2 weeks. While your agent is in the test phase, your handset must be added as a tester.

"Approved RCS agent" means, for delivery: the agent is verified and launched for the recipient's country and carrier, or, for an agent still in its test phase, the recipient is an accepted test device. Elsewhere in these docs "approved RCS agent" is shorthand for exactly that state. A message to a non-RCS recipient still relies on your configured SMS fallback.

The content object carries a type and one matching sub-object:

TypeWhat the recipient seesRCS billing classification
textPlain text, optionally with suggestion chipsRCS Basic (up to 160 chars) or RCS Single (161+); suggested replies keep the length-based class, any suggested action makes it RCS Single
mediaA standalone image or fileRCS Single
rich_cardImage, title, description and up to 4 buttonsRCS Single
carousel2 to 10 swipeable cardsRCS Single

See the full RCS billing classification below.

Q MyBrandAgent Verified
Your order is on its way
Track your delivery using the button below.
Track parcel

A rich card as your verified agent

RCS billing classification

QuickSMS UK RCS billing classification. RCS Basic and RCS Single are QuickSMS commercial billing products for UK RCS, not Google message classifications. The message content and format determine which product applies. The 160-character boundary and the character-counting rule are QuickSMS production billing behaviour, not a universal property of RCS.

Message createdRCS billing classificationPotential SMS fallback
Text, 160 characters or fewerRCS BasicCalculated using SMS encoding and message parts
Text, 161 characters or moreRCS SingleCalculated using SMS encoding and message parts
Rich cardRCS SingleCalculated using the fallback SMS text, encoding and message parts
CarouselRCS SingleCalculated using the fallback SMS text, encoding and message parts
Standalone media (image or file, no card)RCS SingleCalculated using the fallback SMS text, encoding and message parts
Text with suggested repliesRCS Basic to 160 characters, RCS Single 161 or moreCalculated using SMS encoding and message parts
Text with suggested actionsRCS SingleCalculated using SMS encoding and message parts
  • A plain RCS text message containing up to 160 characters is billed as RCS Basic.
  • A plain RCS text message containing 161 characters or more is billed as RCS Single.
  • A rich card is billed as RCS Single. A carousel is billed as RCS Single. Standalone media (an image or file sent without a card) is billed as RCS Single.
  • Adding suggested replies to a text message does not change its classification: it stays RCS Basic to 160 characters and RCS Single at 161 or more.
  • Adding a suggested action (open URL, dial, view location, create calendar event, or open in webview) classifies the message as RCS Single, regardless of length.
  • A text message over 160 characters remains an RCS text message. It does not become a rich card or carousel, and being billed as RCS Single does not make it Rich RCS.
  • Potential SMS fallback is calculated separately from the RCS classification: it depends on the fallback text, its SMS encoding and the number of SMS parts.

Text and suggestions

Plain text (up to 3,072 characters) with tappable chips beneath it. Text without suggestions prices and behaves like a standard RCS text send.

content · text + suggestions
"content": {
  "type": "text",
  "text": "Your delivery arrives tomorrow between 9am and 11am.",
  "suggestions": [
    { "action_type": "open_url", "text": "Track parcel", "url": "https://example.com/track" },
    { "action_type": "dial", "text": "Call us", "phone_number": "+442037408909" }
  ]
}

Up to 11 suggestion chips; labels are limited to 25 characters.

Q MyBrandAgent
Your delivery arrives tomorrow between 9am and 11am.
Track parcel Call us

Text with suggestion chips

Rich cards

At least one of title, description or media is required. The numbered markers show which field paints which part of the card:

content · rich_card
"content": {
  "type": "rich_card",
  "rich_card": {
    "title": "Order dispatched",
    "description": "Your order #12345 is on its way.",
    "orientation": "vertical",
    "media_height": "tall",
    "media": { "url": "https://example.com/hero.png" },
    "buttons": [
      { "action_type": "open_url", "text": "Track parcel", "url": "https://example.com/track" },
      { "action_type": "dial", "text": "Call us", "phone_number": "+442037408909" }
    ]
  }
}
  • 1media, the hero image. HTTPS, publicly reachable, retrieved and cached by QuickSMS.
  • 2title, max 200 characters.
  • 3description, max 2,000 characters.
  • 4buttons, up to 4 per card, labels max 25 characters.
Q MyBrandAgent
1
2Order dispatched
3Your order #12345 is on its way.
4Track parcel
Call us

Rich card anatomy

FieldValuesDefault
orientationvertical | horizontalvertical
thumbnail_alignmentleft | right (horizontal only)left
media_heightshort | medium | tallmedium

Design tip: author vertical-card media to one of the recommended ratios (2:1, 16:9 or 7:3) so it fills the frame; media_height: "tall" suits taller hero imagery. Exact rendering is device-dependent, so anything off-ratio is centre-cropped.

Carousels

2 to 10 swipeable cards, each with its own media and buttons. card_width is small or medium (default medium).

content · carousel
"content": {
  "type": "carousel",
  "carousel": {
    "card_width": "medium",
    "cards": [
      {
        "title": "Standard delivery",
        "description": "Arrives Thursday",
        "media": { "url": "https://example.com/a.png" },
        "buttons": [{ "action_type": "open_url", "text": "Choose", "url": "https://example.com/std" }]
      },
      {
        "title": "Express delivery",
        "description": "Arrives tomorrow",
        "media": { "url": "https://example.com/b.png" },
        "buttons": [{ "action_type": "open_url", "text": "Choose", "url": "https://example.com/exp" }]
      }
    ]
  }
}
Q MyBrandAgent

Recipients swipe between cards

Media

A standalone image or file, optionally with a thumbnail:

content · media
"content": {
  "type": "media",
  "media": {
    "url": "https://example.com/image.png",
    "thumbnail_url": "https://example.com/thumb.png"
  }
}

Requirements, for every media URL on cards and carousels too:

  • QuickSMS requires media URLs to be HTTPS, publicly reachable (no authentication), and served with an image content type.
  • QuickSMS retrieves and processes the asset, and may cache it: reuse the same URL for an identical asset so it is not re-fetched. A broken or redirecting URL can cause a rejection.
  • Use stable CDN URLs, and author media to a recommended vertical-card ratio (2:1, 16:9 or 7:3); carousel media is commonly 4:3. Exact rendering is device-dependent.

Image sizes by media height

media_height sets the rendered height of the card's media area; supply an image whose aspect ratio suits that height, and the handset scales it to the card's width. These ratios are guidance, not a hard requirement: standalone vertical cards present best at 2:1, 16:9 or 7:3, while carousel media is commonly authored at 4:3. Anything else is centre-cropped to fit.

media_heightRendered heightSuggested imageAvailable on
short112 DPWide landscape strip, around 7:3 (e.g. 1440 × 620 px)Standalone cards and carousel cards
medium168 DPLandscape, around 2:1 (e.g. 1440 × 720 px)Standalone cards and carousel cards
tall264 DPLandscape hero around 1,500 px wide, around 16:9 (e.g. 1440 × 810 px)Standalone vertical cards only
  • tall is not supported on carousel cards; carousels use short or medium.
  • tall is also not available on horizontal-orientation cards, where the media sits beside the text.
  • Carousel card_width (small or medium, default medium) sets how much of the screen each card occupies; size images for the medium width and they scale down cleanly to small.
  • Preview any combination without sending: build the card in RCS Studio (the RCS content wizard in the portal), which enforces these limits as you design and exports the matching content JSON.

Common failure: URLs that 301-redirect to another host. Always link the final CDN URL directly, not a vanity or shortened URL.

Actions and replies

Buttons appear on cards (buttons, max 4 per card); suggestion chips appear beneath plain text (suggestions, max 11). Both use the same structure, and labels (text) are limited to 25 characters. Chip styling is fixed by the recipient's messaging app; your agent's brand colour tints the accents.

action_typeOn tapRequired fields
open_urlOpens the URL in the browserurl
open_url_webviewOpens the URL inside the conversationurl; optional webview_mode: full | half | tall
dialOpens the dialler pre-filledphone_number (E.164, e.g. +442037408909)
view_locationOpens the map at a pointlatitude, longitude; optional label
create_calendar_eventOpens the calendar pre-filledstart_time, end_time (ISO 8601), title; optional description
replySends a reply into the conversationNone, see note below
share_locationPrompts the user to share their locationNone, see note below

reply and share_location send data back to you through RCS inbound, configured per Live agent. A tapped reply arrives as message_type: "suggested_reply" with your payload value echoed back as postback_data; a shared location arrives as message_type: "location" with latitude and longitude. Every interaction has a worked inbound example. Configure inbound handling on the agent before relying on these actions.

Webview support varies by the recipient's messaging app: Google Messages shows an in-conversation sheet; other clients may open the browser. As an anti-phishing measure, supported clients can display the destination URL on the chip; this is being rolled out (recent Google Messages) and is not yet universal, so do not rely on it appearing on every device. Design your label so it is clear on its own.

SMS fallback

Rich content can't reach handsets without RCS. The fallback object controls exactly what happens then: you write the SMS rendition, so nothing is improvised on your behalf. Here is the same message on an RCS handset and a non-RCS handset:

Q MyBrandAgent Verified
Order dispatched
Your order #12345 is on its way.
Track parcel

RCS-capable handset: the rich card, status can reach read

MyBrand
Your order #12345 has been dispatched. Track: https://example.com/track

Non-RCS handset: your sms_body, status fallback_delivered

fallback
"fallback": {
  "enabled": true,
  "sms_body": "Your order #12345 has been dispatched. Track: https://example.com/track",
  "sender_id": "MyBrand"
}
FieldRequiredDescription
enabled Optional Enable SMS fallback for this message. Default false.
sms_body Required when enabled The SMS rendition of your rich content, 1 to 1,530 characters. A card cannot fall back without an explicit SMS version.
sender_id Optional SMS sender for the fallback. If omitted, your account's default sender is used (then the first approved sender, then a sanitised agent name).

The delivery and fallback journey:

  • Webhooks and GET /v2/messages/{id} report fell_back_to_sms: true, with terminal status fallback_delivered or fallback_failed.
  • Credits are re-rated at the SMS segment price and the difference adjusted automatically.
  • With enabled: false (or omitted), a non-RCS recipient results in a failed message with a failure.reason; nothing is delivered.

Limitations

  • Rich content requires an approved RCS agent as the sender; anything else is rejected synchronously with SENDER_NOT_RCS_AGENT. Rich content never quietly converts to SMS at send time.
  • Rich content always routes via RCS; smart_routing applies to text content only.
  • Chip icons and styling are fixed by the recipient's messaging app and cannot be customised.
  • reply and share_location interactions are delivered through RCS inbound; configure inbound handling on the agent to receive them.
  • Rich cards, carousels and standalone media are billed as RCS Single. Text with suggested replies keeps its length-based class (RCS Basic to 160 characters, RCS Single at 161+); text with any suggested action is RCS Single. Any SMS fallback is billed separately.

Messaging & Rich RCS APIs Updated 21 July 2026 · View changelog →

Choose a messaging channel

Three channels, one decision. Pick here, then every messaging page shows you a complete, channel-specific journey: request, response, delivery receipts, errors and billing.

SMSRCS TextRich RCS
What it isPlain text to any mobile, worldwideThe same text, delivered as RCS with read receipts where the handset supports it, SMS otherwiseBranded cards, carousels, media and interactive buttons in the native inbox
APIMessaging API · POST /v1/messagesMessaging API · POST /v1/messages with smart_routingRich RCS API · POST /v2/messages
PrerequisitesApproved sender IDApproved RCS agent + SMS fallback senderApproved RCS agent (+ fallback sender for the SMS rendition)
ReachesEvery mobileEvery mobile (RCS or SMS)RCS-capable handsets; everyone else via your explicit SMS fallback
Read receiptsNoYes, when delivered as RCSYes, when delivered as RCS
Delivery receiptDLR with GSM status codesDLR; GSM code on SMS fallback, read receipt on RCSmessage.status payload with failure.reason
BillingPer SMS message partRCS text by length (RCS Basic to 160 chars, RCS Single 161+); SMS parts on fallbackRCS Single per message for rich cards, carousels and standalone media; SMS parts on fallback
Best forOne-time codes, alerts, universal reachEveryday notifications with better engagement at SMS-grade reachBranded journeys: order tracking, boarding passes, promotions

Unsure? Start with SMS: it reaches everyone, and RCS text is one field away once your RCS agent is approved. Rich RCS is fully additive; adopting it changes nothing about your existing sends.

Two things share the word "text". RCS text is a channel on the Messaging API (POST /v1/messages with smart_routing); by length it is calculated as the RCS Basic or RCS Single billing product. Rich RCS text is the text content type of the Rich RCS API (POST /v2/messages), which also carries the Rich RCS lifecycle and suggestion model. Sending 161 characters on RCS text changes the calculated product to RCS Single but never changes the channel, API or endpoint.

How RCS is billed

QuickSMS UK RCS billing classification. RCS Basic and RCS Single are QuickSMS commercial billing products for UK RCS, not Google message classifications. The message content and format determine which product applies. The 160-character boundary and the character-counting rule are QuickSMS production billing behaviour, not a universal property of RCS.

Message createdRCS billing classificationPotential SMS fallback
Text, 160 characters or fewerRCS BasicCalculated using SMS encoding and message parts
Text, 161 characters or moreRCS SingleCalculated using SMS encoding and message parts
Rich cardRCS SingleCalculated using the fallback SMS text, encoding and message parts
CarouselRCS SingleCalculated using the fallback SMS text, encoding and message parts
Standalone media (image or file, no card)RCS SingleCalculated using the fallback SMS text, encoding and message parts
Text with suggested repliesRCS Basic to 160 characters, RCS Single 161 or moreCalculated using SMS encoding and message parts
Text with suggested actionsRCS SingleCalculated using SMS encoding and message parts
  • A plain RCS text message containing up to 160 characters is billed as RCS Basic.
  • A plain RCS text message containing 161 characters or more is billed as RCS Single.
  • A rich card is billed as RCS Single. A carousel is billed as RCS Single. Standalone media (an image or file sent without a card) is billed as RCS Single.
  • Adding suggested replies to a text message does not change its classification: it stays RCS Basic to 160 characters and RCS Single at 161 or more.
  • Adding a suggested action (open URL, dial, view location, create calendar event, or open in webview) classifies the message as RCS Single, regardless of length.
  • A text message over 160 characters remains an RCS text message. It does not become a rich card or carousel, and being billed as RCS Single does not make it Rich RCS.
  • Potential SMS fallback is calculated separately from the RCS classification: it depends on the fallback text, its SMS encoding and the number of SMS parts.

Messaging API Updated 21 July 2026 · View changelog →

Sending controls

The platform applies several safeguards between your API call and the handset: scheduling holds, out-of-hours rules, anti-flood protection and country restrictions. They apply to every send, whichever endpoint accepts it. This page explains each control, when it triggers, and what you see when it does.

Held messages and scheduling

A message that is accepted but not yet released for dispatch is held. Held messages carry a scheduled send time (scheduled_send_at) telling you when the platform will release them, and credits are reserved immediately so the send cannot later fail on balance. No action is needed on your side; the message dispatches automatically when the hold clears.

Out-of-hours protection

Accounts can restrict dispatch to a defined business-hours window, preventing a late-night batch job from messaging your customers at 3am. The account chooses what happens to a message submitted outside the window:

ModeAPI behaviourCredits
Reject The message is rejected with code OUT_OF_HOURS. The response includes the allowed window and the time sending resumes. Not deducted
Hold The message is accepted as held and dispatched automatically when the window opens, with the release time reported as scheduled_send_at (the Messaging API reports status HELD). Reserved immediately
Hold mode · 202, Messaging API response
{
  "messages": [{
    "quicksms_id": "msg_80fac5c752941b27",
    "status": "HELD",
    "scheduled_send_at": "2026-07-15T08:00:00+00:00"
  }]
}
Reject mode · 422
{
  "messages": [{
    "status": "REJECTED",
    "error": {
      "code": "OUT_OF_HOURS",
      "message": "Outside the allowed sending window (08:00 to 20:00). Sending resumes at 2026-07-15T08:00:00+00:00."
    }
  }]
}

Note the distinction: scheduled_send_at is a response field reporting when a held message will be released; it is not a request field for future scheduling (that is the planned send_at, coming soon). Configure the window and mode in your account settings; if none is configured, sends are released immediately at any time of day. Out-of-hours restrictions are bypassed in sandbox mode.

Anti-flood protection

Anti-flood protection prevents duplicate messages reaching the same recipient within a configurable window, which typically indicates a stuck retry loop or an integration bug rather than intent. A duplicate is the same account sending identical content to the same mobile number within the window. The account's anti-flood mode decides what happens:

ModeBehaviour
offNo duplicate checking.
enforceDuplicates are rejected with code DUPLICATE_MESSAGE; the response includes the original message's ID.
monitorDuplicates are logged but the message is still sent.
Enforce mode · duplicate rejected · 422
{
  "messages": [{
    "status": "REJECTED",
    "error": {
      "code": "DUPLICATE_MESSAGE",
      "message": "Identical content was sent to this recipient within the anti-flood window. Original message: msg_80fac5c752941b27."
    }
  }]
}

The window length and mode are configured per account in your security settings. Anti-flood is bypassed in sandbox mode. Use an Idempotency-Key so that retries replay the original response instead of ever looking like a flood.

Country restrictions

Every destination country has a permission status on your account. The recipient's country is determined from the mobile number's dialling prefix (44 is the UK, 353 is Ireland, and so on); per-account overrides are checked first, then the platform-wide default. A send to a blocked destination is rejected synchronously with code COUNTRY_NOT_ALLOWED, before any carrier contact, and nothing is billed. Restrictions exist for fraud protection and compliance; international destinations beyond your agreed set are blocked by default. Review your destinations in your account security settings, and contact your account manager or support to enable additional countries; changes take effect immediately once approved.

Sandbox mode

Sandbox mode simulates the full send-and-deliver flow, including webhooks, without carrier delivery or credit use. Three things activate it:

MethodDescription
Use an sk_test_ keyAlways forces sandbox mode.
Set "sandbox": trueForces sandbox behaviour for that request, with any key type.
Connection environment set to testConfigured on the API connection in the portal.
BehaviourProductionSandbox
Real SMS and RCS deliveryYesNo
Credits deductedYesNo
Anti-flood checksActiveBypassed
Out-of-hours checksActiveBypassed
Delivery webhooksReal carrier receiptsSimulated after 3 to 5 seconds
RCS read receiptsFrom the recipient's deviceSimulated after 5 to 10 seconds

Test keys can only send to mobile numbers in your approved test allowlist, managed on the API connection in the portal; other numbers are rejected with TEST_NUMBER_NOT_APPROVED. The text "This is a QuickSMS test message" is appended to sandbox sends, and test mode uses the default QuickSMS test sender unless you have an approved sender of your own. You can force specific simulated outcomes with the recipient number's ending, see simulated outcomes in test mode.

Callback URL priority

Delivery receipts are sent to the most specific webhook URL configured:

  1. callback_url supplied on the individual message, if any.
  2. Otherwise, the webhook URL configured on the API connection that sent it.

If neither is configured, no webhook is sent and you can still reconcile with GET /v2/messages/{message_id}.

Inbound API V2 Current · Live · SMS and RCS inbound available· Updated 21 July 2026 · View changelog →

Inbound messaging

Receive customer replies through QuickSMS and route each inbound message to the systems and teams that need it. SMS replies arrive through Inbound V2; replies to your RCS agent arrive through RCS inbound, configured per agent.

Inbound overview

Inbound messaging has three parts: an inbound number that receives the message, the routing options you enable on that number, and the delivery destinations each message is sent to. One inbound SMS can reach several destinations at once: your webhook for integrations, the QuickSMS Inbox for your team, and an email inbox for anyone else, while opt-out keywords are processed automatically.

Use inbound messaging for:

  • Two-way conversations: let customers reply to your messages and route those replies into your CRM, support tool or inbox.
  • Opt-out handling: capture STOP, UNSUBSCRIBE and similar keywords automatically.
  • Lead capture: advertise a number on print, outdoor or broadcast media and route responses into your systems.
  • Operational notifications: receive alerts that automated systems can act on.

How inbound numbers work

An inbound number is a dedicated UK mobile number assigned to your QuickSMS account. Customers can reply to messages sent from that number, or send messages to it directly. QuickSMS receives the inbound SMS and routes it to each destination you have enabled.

In the portal and some existing integrations, inbound numbers may also be referred to as Virtual Mobile Numbers or VMNs. Sends from an alphanumeric sender ID cannot receive replies; use an inbound number as the sender whenever you expect a response.

Inbound numbers are managed in Management › Numbers: click a number's row to open its configuration drawer, or select several numbers and use Bulk Configure to apply the same settings to all of them in one save.

Bulk configure overwrites per-number settings. If you bulk-configure ten numbers to one webhook URL, every selected number loses its previous, potentially different, URL. Review the impact before saving.

Receive SMS replies

The end-to-end journey, from number to processed reply:

  1. Obtain or select an inbound number in Management › Numbers.
  2. Enable the routing options you need on that number.
  3. Configure an inbound webhook where your systems need the message, and store the signing secret.
  4. Send from the inbound number and receive an SMS reply.
  5. Verify the webhook signature on the request you receive.
  6. Process the inbound message in your system.
  7. Opt-out keywords are handled automatically where you have enabled opt-out processing.
  8. Review the message in the portal Inbox or the forwarded email, whichever destinations you enabled.

Routing inbound messages

Each inbound number has four routing options. Three are delivery destinations that receive a copy of the message; the fourth, automatic opt-out processing, is a processing action that updates your suppression lists rather than delivering the message anywhere. The portal groups all four together in the number's configuration drawer.

Routing optionKindWhat it doesBest for
QuickSMS Inbox Delivery destination Conversational view inside the portal, with read state and reply-from-portal. Teams who reply to customers directly from the portal.
Webhook Delivery destination HTTPS POST to your endpoint with the message payload, HMAC-signed. System integrations, CRMs, custom backends.
Email forwarding Delivery destination Sends inbound messages to one or many email addresses, immediately or as a digest. Teams without engineering capacity who want messages in their inbox.
Automatic opt-out processing Processing action STOP-style keywords automatically add the sender to a configured opt-out list. Any campaign sender; required for compliance.

Routing options are independent. Every enabled option runs automatically on each inbound message, no polling or triggering required. A single message can appear in the Inbox, post to your webhook, forward to email and trigger an opt-out check simultaneously; a failure in one destination does not affect the others, and each routing action is recorded in the audit log. Disabling an option pauses it without deleting its configuration: the webhook keeps its URL and signing secret, email forwarding keeps its recipient list and delivery mode, and detaching an opt-out list leaves the list itself intact, so re-enabling resumes with the same settings.

What each option needs

Routing optionRequired to enable
QuickSMS InboxToggle on. No other configuration.
WebhookAn HTTPS forwarding URL. The signing secret is generated automatically.
Email forwardingOne or more email addresses (semicolon-separated). Delivery mode defaults to Immediate.
Automatic opt-out processingSelect an opt-out list from the dropdown.

QuickSMS Inbox

Messages from the same sender thread into one conversation, even across days or weeks, with unread counts, full-text search across participants and content, and reply-from-portal (the reply is sent from the inbound number). Multiple portal users can work from the same Inbox, subject to permissions; sub-account users only see conversations on numbers they have access to. Toggling the Inbox off skips it without affecting the other routing options.

Email forwarding

Choose how messages arrive based on volume and urgency:

ModeWhat arrivesTiming
ImmediateOne email per inbound message.Within seconds of the message arriving.
Hourly digestOne email with all messages from the hour.The first message in a batch starts the clock; a scheduled job checks every minute, so the digest normally lands 60 to 61 minutes after that first message.
Daily digestOne email with all messages from the day.The same pattern at 1,440 minutes: normally 24 hours to 24 hours 1 minute after the first message in the batch.
  • Forward to one address or a whole team: separate addresses with semicolons (up to 1,000 characters of addresses). Each address is validated independently, and each recipient gets their own copy with no cross-recipient visibility.
  • Delivery mode defaults to Immediate, and switching between Immediate, hourly digest and daily digest keeps the same recipient list; only the cadence changes.
  • Subject lines are privacy-conscious ("New SMS received"); message content never appears in the subject. The email body contains the original sender's mobile number, the receiving inbound number, the message content and the time received.
  • Forwarding is one-way: replies to the email are not delivered back to the original SMS sender.

Automatic opt-out processing

When an inbound message contains a recognised opt-out keyword, the sender is added to the configured opt-out list with a timestamp and source, the event is recorded in the audit log, and the sender is excluded from future campaigns. The message still reaches your other enabled destinations, so you keep a record of the opt-out. Recognised keywords, case-insensitive and tolerant of surrounding punctuation:

Opt-out keywords
STOP        STOPALL      UNSUBSCRIBE
CANCEL      END          QUIT
OPT OUT     OPTOUT       REMOVE

A repeated opt-out from an already opted-out number is detected and recorded without error, so repeated STOP messages are never a failure. Point each number at its own opt-out list or share one across numbers. The master opt-out list is account-wide: numbers on it are excluded from every campaign regardless of which list captured them, and cannot be marketed to again without explicit re-consent. Automatic STOP handling is required by PECR for marketing SMS, so attach an opt-out list to every campaign-receiving number. See opt-outs and consent for the API side.

Inbound webhook V2

The webhook is the API surface of inbound messaging: every inbound message on a webhook-enabled number is POSTed to your endpoint in real time as JSON, signed so you can verify it came from QuickSMS. The forwarding URL must be HTTPS; plain HTTP URLs are not accepted. The signing secret is generated when you first save a URL and is displayed once, so copy it then. You can rotate the secret on demand (the old secret stops working immediately); secrets do not expire on their own, and disabling the webhook keeps both the URL and the secret so re-enabling resumes with the same configuration.

Inbound webhook request
POST /your-webhook-endpoint HTTP/1.1
Content-Type: application/json
X-QuickSMS-Signature: sha256=<hex digest>
X-QuickSMS-Timestamp: 1718625684

{
  "from": "+447891658398",
  "to": "447908679883",
  "content": "Hi, just checking when my order will arrive?",
  "channel": "sms",
  "message_id": "sNb2FSG8wb6IsArvRdbS",
  "timestamp": "2026-06-17T11:41:24Z"
}

Signature verification

Compute an HMAC-SHA256 over timestamp + "." + body using your signing secret and compare it to the hex digest in X-QuickSMS-Signature (note the sha256= prefix). Verify against the raw request bytes, before any JSON parsing, and use a constant-time comparison:

Verify an inbound webhook
const crypto = require('crypto');

function verifyInbound(rawBody, timestampHeader, signatureHeader, secret) {
  const expected = crypto.createHmac('sha256', secret)
    .update(timestampHeader + '.' + rawBody)
    .digest('hex');
  const received = signatureHeader.replace(/^sha256=/, '');
  return crypto.timingSafeEqual(
    Buffer.from(expected, 'hex'),
    Buffer.from(received, 'hex')
  );
}
Verify an inbound webhook
function verifyInbound(string $rawBody, string $timestampHeader, string $signatureHeader, string $secret): bool
{
    $expected = hash_hmac('sha256', $timestampHeader . '.' . $rawBody, $secret);
    $received = preg_replace('/^sha256=/', '', $signatureHeader);
    return hash_equals($expected, $received);
}
Verify an inbound webhook
import hmac, hashlib

def verify_inbound(body_bytes, timestamp_header, signature_header, secret):
    expected = hmac.new(
        secret.encode(),
        (timestamp_header + "." + body_bytes.decode()).encode(),
        hashlib.sha256,
    ).hexdigest()
    received = signature_header.replace("sha256=", "", 1)
    return hmac.compare_digest(expected, received)
Verify an inbound webhook
using System.Security.Cryptography;
using System.Text;

static bool VerifyInbound(string rawBody, string timestamp, string signatureHeader, string secret)
{
    using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
    var expected = Convert.ToHexString(
        hmac.ComputeHash(Encoding.UTF8.GetBytes($"{timestamp}.{rawBody}"))
    ).ToLowerInvariant();
    var received = signatureHeader.StartsWith("sha256=")
        ? signatureHeader["sha256=".Length..]
        : signatureHeader;
    return CryptographicOperations.FixedTimeEquals(
        Convert.FromHexString(expected), Convert.FromHexString(received));
}
Verify an inbound webhook
static boolean verifyInbound(String rawBody, String timestamp,
                             String signatureHeader, String secret) throws Exception {
    var mac = javax.crypto.Mac.getInstance("HmacSHA256");
    mac.init(new javax.crypto.spec.SecretKeySpec(
        secret.getBytes(java.nio.charset.StandardCharsets.UTF_8), "HmacSHA256"));
    byte[] expected = mac.doFinal(
        (timestamp + "." + rawBody).getBytes(java.nio.charset.StandardCharsets.UTF_8));

    String received = signatureHeader.startsWith("sha256=")
        ? signatureHeader.substring("sha256=".length())
        : signatureHeader;
    byte[] receivedBytes = java.util.HexFormat.of().parseHex(received);
    return java.security.MessageDigest.isEqual(expected, receivedBytes);
}
Verify an inbound webhook
func verifyInbound(rawBody []byte, timestamp, signatureHeader, secret string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write([]byte(timestamp + "."))
    mac.Write(rawBody)
    expected := hex.EncodeToString(mac.Sum(nil))

    received := strings.TrimPrefix(signatureHeader, "sha256=")
    return hmac.Equal([]byte(expected), []byte(received))
}
Verify an inbound webhook
require "openssl"

def verify_inbound(raw_body, timestamp, signature_header, secret)
  expected = OpenSSL::HMAC.hexdigest("SHA256", secret, "#{timestamp}.#{raw_body}")
  received = signature_header.delete_prefix("sha256=")
  OpenSSL.secure_compare(expected, received)
end

Failure behaviour

  • Your endpoint should respond with a 2xx within 10 seconds.
  • A non-2xx response or timeout is logged as a failed delivery for the webhook destination and is not retried automatically. This is different from outbound delivery receipts, which retry on a schedule. Enable the QuickSMS Inbox or another routing option so the message remains accessible if your endpoint is unavailable.
  • Failures are isolated: the Inbox, email forwarding and opt-out processing still run normally.
  • De-duplicate on message_id if your endpoint may be called more than once.

Payload reference

Inbound webhook V2 delivers the following JSON payload for each inbound SMS. The Inbox and email forwarding present the same underlying message record in their own formats.

FieldTypeDescription
fromstringThe number that sent the message, in E.164 format (e.g. +447891658398).
tostringThe inbound number that received the message. Currently returned as digits without the leading + (e.g. 447908679883), unlike from; normalising both fields to E.164 is under consideration as a contract change.
contentstringThe text body of the SMS, UTF-8.
channelstringsms. This flat payload carries SMS; RCS replies are delivered through the RCS agent inbound webhook, which uses its own enveloped payload with "channel": "rcs".
message_idstringUnique identifier for the inbound message; use it for de-duplication.
timestampstringISO 8601 UTC timestamp of receipt at QuickSMS.

Receiving RCS replies? RCS inbound is live, configured per agent, and delivered with a richer enveloped payload (typed replies, chip and button taps, photos and files, shared locations). It is documented in full under RCS inbound · replies to your RCS agent below, including a worked example for every interaction type.

Inbound V1 (deprecated)

Which inbound system am I on?

  • New SMS inbound integration: build against Inbound V2 (the HMAC-signed normalised webhook above).
  • New RCS inbound integration: configure RCS agent inbound on your Live agent (per-agent webhook, Inbox, email and opt-out handling).
  • On an older integration (SMS or RCS)? That is Inbound V1. It keeps working, but it is now deprecated: plan the migration below.

Inbound V1 is the original inbound delivery from the same platform generation as the Legacy API. It remains in service for the integrations built on it and nothing changes without warning, but it is deprecated: every V1 capability now has a current, HMAC-signed successor, and no new V1 integrations should be created. No removal date is set.

The full V1 payload reference now lives with the platform history: Inbound V1 reference (deprecated).

Migrating from Inbound V1

  • SMS replies: move to the Inbound V2 webhook. You gain the HMAC-signed normalised payload (from, to, content, channel, message_id, timestamp) in place of the unsigned V1 shape; de-duplicate on message_id.
  • RCS replies: enable RCS agent inbound on your Live agent. You gain typed replies, chip and button taps with postback_data, photo/file replies and shared locations, none of which V1 delivers in structured form.
  • If you want to verify the new payloads before retiring the old endpoint, ask your account manager about running V1 and the new delivery in parallel during the migration.

RCS inbound · replies to your RCS agent

When someone replies to a message from your RCS agent, that reply is an inbound message. Inbound handling is configured per agent: in Management › RCS Agents V2, open the actions menu on a Live agent and choose Inbound configuration. The same four destinations as SMS inbound apply, in any combination, independently: the in-portal Inbox (on by default), opt-out handling against a list you choose, webhook delivery to your own HTTPS endpoint, and one-way email forwarding. If an agent is suspended, its inbound configuration pauses and resumes with the agent; nothing is lost.

Webhook signing secret. The first time you save a forwarding URL, QuickSMS generates a signing secret and shows it once; store it safely. If it is ever exposed, use Rotate signing secret: the old secret stops working immediately.

The RCS inbound webhook

Each inbound message is delivered to your forwarding URL as an HTTPS POST with Content-Type: application/json and two headers: x-quicksms-signature (sha256=<hex>, an HMAC-SHA256 of the raw request body computed with your signing secret) and x-quicksms-timestamp (Unix seconds, for replay checks). Verify the signature against the raw bytes before parsing, and reject stale timestamps.

This signature base differs from other QuickSMS webhooks. Delivery receipts and the flat Inbound V2 SMS payload sign {timestamp}.{body}; the RCS agent inbound webhook signs the body only. Use the right base per webhook or verification will fail. Converging on a single base is under engineering review; any change will be announced in the changelog before it takes effect.

The body is an envelope: event (always inbound_message), source (rcs_agent for per-agent delivery; api_connection when an account-level API connection's inbound webhook also receives the message, each with its own signing secret), and data:

FieldDescription
data.fromThe sender's number, E.164 (e.g. +447700900123).
data.toThe name of the RCS agent that received the message (an agent name, not a number).
data.contentThe text of the reply; for a button tap, the button's visible label; for a shared location, the marker [RCS interaction: LOCATION].
data.channelrcs. Distinguishes RCS replies from SMS.
data.message_idUnique ID for this inbound message; use it for de-duplication.
data.timestampWhen the message was received (ISO 8601 with offset).
data.message_typetext (typed reply), suggested_reply or suggested_action (a chip/button tap), file (a photo or file), location (a shared position), or other. This is how you tell a reply from a tap.
data.interaction_typeBroader context from the RCS network about where the interaction happened, e.g. BASIC_MESSAGE or RICH_CARD_RESPONSE (a tap on a rich card).
data.postback_dataFor taps, the payload attached to the button when it was sent (the outbound reply action's payload field). null for typed replies.
data.media_urlFor a file/image reply, a link to the file on the RCS network's storage, not on QuickSMS; QuickSMS forwards the link and does not download or host the file. Fetch it promptly if you need to keep it. null otherwise.
data.media_name / data.media_content_type / data.media_sizeThe file's original name, its type as reported with the file (may be a bare format such as jpeg rather than a full MIME type), and its size in bytes.
data.latitude / data.longitudeFor a shared location, the coordinates. null otherwise.

Treat any field not listed above as diagnostic: it is not part of the stable contract, may change or disappear without notice, and should never be branched on. Build against message_type.

What each interaction looks like

One payload shape covers every way a recipient can respond. Read data.message_type first; fields that do not apply to an interaction are null. In brief:

The recipient…message_typeWhere the signal is
Typed a free-text replytextcontent is what they wrote
Tapped a suggested reply chipsuggested_replycontent is the chip label; postback_data is your payload
Tapped a button (open URL, dial, calendar)suggested_actioncontent is the button label; the handset performs the action locally
Sent a photo or filefilemedia_url, media_name, media_content_type, media_size
Shared their locationlocationlatitude and longitude

1 · A typed, free-text reply

Inbound · typed reply
{
  "event": "inbound_message",
  "source": "rcs_agent",
  "data": {
    "from": "+447700900123",
    "to": "MyBrandAgent",
    "content": "What time will my order arrive?",
    "channel": "rcs",
    "message_id": "ib_7f3a9c2e5d81",
    "timestamp": "2026-07-21T09:15:02+01:00",
    "message_type": "text",
    "interaction_type": "BASIC_MESSAGE",
    "postback_data": null
  }
}

2 · A suggested-reply chip tap

The value you set in the outbound reply action's payload field comes back as postback_data; content is the chip's visible label. Drive your logic from postback_data, not the label, which you may reword at any time.

Inbound · suggested reply tap
{
  "event": "inbound_message",
  "source": "rcs_agent",
  "data": {
    "from": "+447700900123",
    "to": "MyBrandAgent",
    "content": "Yes, sign me up",
    "channel": "rcs",
    "message_id": "ib_2b9d4e7a1c53",
    "timestamp": "2026-07-21T09:16:40+01:00",
    "message_type": "suggested_reply",
    "interaction_type": "BASIC_MESSAGE",
    "postback_data": "OPTIN_YES"
  }
}

3 · A button tap: open URL, dial, or add to calendar

Taps on open_url, dial and create_calendar_event buttons arrive as message_type: "suggested_action" with the button's label in content. The handset performs the action itself (opens the browser, pre-fills the dialler, opens the calendar); the webhook simply tells you the tap happened. A button tapped on a rich card reports interaction_type: "RICH_CARD_RESPONSE".

Inbound · open URL button tap
{
  "event": "inbound_message",
  "source": "rcs_agent",
  "data": {
    "from": "+447700900123",
    "to": "MyBrandAgent",
    "content": "Track parcel",
    "channel": "rcs",
    "message_id": "ib_9c1e6b3f8d24",
    "timestamp": "2026-07-21T09:18:11+01:00",
    "message_type": "suggested_action",
    "interaction_type": "RICH_CARD_RESPONSE",
    "postback_data": null
  }
}

A dial tap looks the same with "content": "Call us"; a calendar tap with "content": "Add to calendar". Pending confirmation The exact postback_data value for action buttons (which have no payload field on the outbound contract) is being confirmed; treat the label in content as the reliable signal for now.

4 · A photo or file reply

QuickSMS forwards a link to the file on the RCS network's storage; it does not download or host the file. Fetch it from media_url promptly if you need to keep it. Pending confirmation The content marker for file replies and the link's lifetime are being confirmed.

Inbound · photo reply
{
  "event": "inbound_message",
  "source": "rcs_agent",
  "data": {
    "from": "+447700900123",
    "to": "MyBrandAgent",
    "content": "[RCS interaction: FILE]",
    "channel": "rcs",
    "message_id": "ib_4a8d2c7e9f16",
    "timestamp": "2026-07-21T09:20:33+01:00",
    "message_type": "file",
    "interaction_type": "BASIC_MESSAGE",
    "postback_data": null,
    "media_url": "https://rcs-media.example/files/dl/6f2e8a91c4b7",
    "media_name": "photo.jpg",
    "media_content_type": "jpeg",
    "media_size": 245678
  }
}

5 · A shared location

When the recipient taps your share_location suggestion (or shares a position directly), the coordinates arrive in latitude and longitude, and content carries the marker [RCS interaction: LOCATION].

Inbound · shared location
{
  "event": "inbound_message",
  "source": "rcs_agent",
  "data": {
    "from": "+447700900123",
    "to": "MyBrandAgent",
    "content": "[RCS interaction: LOCATION]",
    "channel": "rcs",
    "message_id": "ib_1d6f9b4a3e82",
    "timestamp": "2026-07-21T09:22:05+01:00",
    "message_type": "location",
    "interaction_type": "BASIC_MESSAGE",
    "postback_data": null,
    "latitude": 51.5074,
    "longitude": -0.1278
  }
}

Verifying the RCS inbound signature

Compute an HMAC-SHA256 over the raw request body with your signing secret and compare it, constant-time, to the header value after the sha256= prefix. Reject mismatches, and reject requests whose x-quicksms-timestamp is older than your tolerance:

Verify · pseudocode
expected = "sha256=" + hmac_sha256(secret, raw_request_body)
if constant_time_equals(expected, request.header("x-quicksms-signature")):
    # trusted

Respond promptly with a success status. The retry behaviour when your endpoint is unreachable is being confirmed and will be documented here.

Contact Book API v1.1 Updated 21 July 2026 · View changelog →

Contacts & audiences

Contacts are the audience layer of the messaging platform: who you can message, how they're organised, and what they've consented to. The Contact Book API manages contacts, tags, lists, opt-outs and custom fields, 47 endpoints, all following the same conventions as the rest of the platform.

Credential: the Contact Book uses its own scoped token (cbk_… / cbk_test_…), see the credential matrix. Base URL: https://api.quicksms.com/v1.

The audience workflow

Contacts connect directly to messaging. A typical end-to-end journey:

1. Create a contact

Only mobile_number is required. Everything else, name, email, your CRM reference, custom fields, is optional.

Mobile numbers are normalised on input. Whether you submit +447700900123, 07700 900123 or (0044) 7700-900123, the contact is stored canonicalised to E.164. Lookups and de-duplication work against the canonical form, so formatting variation in your imports won't create duplicates.

2. Organise with lists and tags

Lists are explicit audiences ("Newsletter subscribers"); tags are labels for filtering ("VIP"). Add up to 1,000 contacts per call, import up to 5,000 at a time, or apply changes to an entire filtered audience asynchronously with the by-filter operations.

3. Message the audience

Send to individual contacts through the messaging API, using the contact's stored data for personalisation and customer_id to correlate. For whole lists and segments, run campaigns from the portal, list-addressed API sends arrive with the batch messaging API Coming soon.

4. Receive delivery receipts

Delivery, read and failure receipts flow to your webhook endpoint as usual, see Delivery receipts & webhooks. Use them to keep engagement data on your side in sync.

A contact's status is computed from opt-out records: active or opted_out. Check before you send, and record opt-outs the moment they happen.

Opt-outs and consent

Opt-out lists are suppression lists. Numbers on them are excluded from portal campaigns automatically, and the API gives you everything needed to enforce the same rule in your own sends:

  • Check a number before sending: the opt-out status check endpoint tells you whether a mobile number appears on any of your opt-out lists.
  • Record an opt-out: add the number to an opt-out list the moment a recipient asks to stop.
  • Bulk operations: import opt-out records, move them between lists, or opt out up to 1,000 contacts per call.

Custom fields

Define your own contact attributes (a loyalty tier, a renewal date) once, then read and write them per contact through custom_data. Definitions are managed through the custom-fields endpoints and available across imports, filters and the portal UI.

Bulk and asynchronous operations

Three sizes of write, one pattern each:

ScaleUseBehaviour
One record Standard CRUD endpoints Synchronous. 201/200 with the resource.
Up to 1,000 bulk endpoints (import, tag, list, opt-out, delete) Synchronous. Returns an affectedCount.
Unlimited (whole filtered audience) by-filter endpoints Asynchronous. 202 Accepted with an operation_id; poll GET /v1/operations/{id} for progress and the final result.

Send an Idempotency-Key on bulk writes, an accidental retry of a 50,000-contact tagging operation should replay the original response, not run twice. See Idempotency.

Messaging API & Rich RCS Updated 21 July 2026 · View changelog →

Delivery receipts & webhooks

Delivery receipts (DLRs) are how the platform talks back: when a message is delivered, read, fails or falls back to SMS, QuickSMS delivers the receipt as a signed webhook POSTed to your endpoint. One transport, one signature scheme for every channel; only the payload shape differs by API version.

Which payload will you receive?

The payload shape is determined solely by which endpoint accepted the message, never by account settings or content. The transport, retry schedule and signature scheme are identical for both, so one verification implementation covers everything.

Message accepted byPayload you receiveDocumented
Messaging API · POST /v1/messages Delivery receipt (DLR): uppercase statuses, quicksms_id, GSM status codes. Sent when the message reaches a final status Messaging API reference
Rich RCS · POST /v2/messages message.status payload: lowercase statuses, message_id, normalised failure.reason. Sent per status transition This page

What each channel's receipts can tell you:

CapabilitySMSRCS TextRich RCS
Delivered resultYesYesYes
Read receiptNoYes, on RCS deliveryYes, on RCS delivery
GSM status codeYesOn SMS fallbackOn SMS fallback
RCS failure informationNoLimitedYes, failure.reason
Actual delivered channelSMSRCS or SMSRCS or SMS
SMS message partsYesOn fallbackOn fallback
Compare messaging channels →

SMS delivery receipts

SMS sends receive the Messaging API delivery receipt when the message reaches a final status, carrying the uppercase message_status, the GSM status_code, the billable message_parts and your customer_id / metadata. The complete field table lives in the Messaging API reference; these are current production receipts, not legacy.

The SMS message lifecycle

Alternative outcomes:

StatusWhereFinal?Meaning
ACCEPTEDSend responseNoValidated and queued; wait for the receipt
HELDSend responseNoDelayed by out-of-hours; released at scheduled_send_at
REJECTEDSend responseYesRefused synchronously; no receipt follows
PENDINGReceiptNoAwaiting carrier confirmation
DELIVEREDReceiptYesConfirmed on the handset (GSM code 0)
UNDELIVEREDReceiptYesDelivery failed; the GSM code says why
EXPIREDReceiptYesValidity period elapsed (GSM code 255)
REJECTEDReceiptYesRejected by the carrier (e.g. GSM code 13)
Delivered · GSM code 0
{
  "quicksms_id": "msg_80fac5c752941b27",
  "customer_id": "cust_abc123",
  "channel": "SMS",
  "message_status": "DELIVERED",
  "status_code": 0,
  "message_parts": 2,
  "done_time": "2026-07-15T12:00:03+00:00",
  "metadata": { "order_id": "12345" }
}
// message_parts: 2 means this message was billed as two SMS messages.
Failed · absent subscriber (27) and expired (255)
{ "quicksms_id": "msg_80fac5c752941b27", "channel": "SMS",
  "message_status": "UNDELIVERED", "status_code": 27, "message_parts": 1,
  "done_time": "2026-07-15T12:48:00+00:00" }

{ "quicksms_id": "msg_11ab2c3d4e5f6a7b", "channel": "SMS",
  "message_status": "EXPIRED", "status_code": 255, "message_parts": 1,
  "done_time": "2026-07-17T12:00:00+00:00" }

The GSM status codes explain the outcome (0 delivered, 1 invalid destination, 13 rejected by mobile network, 27 absent subscriber, 32 network error, 255 unknown or expired). Headers, signing and retries are shared, see below.

RCS text delivery receipts

Smart-routed sends receive the same Messaging API delivery receipt, with the channel field reporting the route actually used. RCS-delivered messages can add a read receipt; messages that fell back to SMS behave exactly like SMS receipts, GSM code and message_parts included.

The RCS text message lifecycle

The fallback path, when the handset is not RCS-capable or reachable:

The statuses are the same uppercase vocabulary as SMS; what RCS text adds is the channel outcome (RCS or SMS), the read receipt fields on RCS delivery, and the GSM code plus message_parts whenever the SMS fallback carried the message. No read receipt ever appears for SMS-delivered messages.

Delivered over RCS, then read
{
  "quicksms_id": "msg_5c11a09eb7f2d846",
  "channel": "RCS",
  "message_status": "DELIVERED",
  "status_code": 0,
  "message_parts": 1,
  "read_receipt": true,
  "read_receipt_timestamp": "2026-07-15T12:01:10+00:00",
  "done_time": "2026-07-15T12:00:04+00:00"
}
Delivered through SMS fallback · 2-part SMS
{
  "quicksms_id": "msg_5c11a09eb7f2d846",
  "channel": "SMS",
  "message_status": "DELIVERED",
  "status_code": 0,
  "message_parts": 2,
  "read_receipt": null,
  "done_time": "2026-07-15T12:00:06+00:00"
}
// Fallback delivered as SMS: billed as two SMS parts, calculated independently.

A failed fallback reports message_status: "UNDELIVERED" with the GSM code, exactly like SMS. The full field table is in the Messaging API reference, and the routing decision itself is explained under how smart routing works.

The Rich RCS message lifecycle

Messages accepted by POST /v2/messages move through one lowercase status vocabulary, the same in send responses, retrieval and webhooks (the Messaging API uses its own uppercase vocabulary):

Alternative paths:

StatusFinal?MeaningChannels
queuedNoAccepted and queued for dispatchAll
submittedNoHanded to the mobile network for deliveryAll
deliveredYesConfirmed on the handsetAll
readYesRead by the recipientRCS text, Rich RCS
failedYesDelivery failed, see the failure objectAll
expiredYesValidity period elapsed before deliveryAll
rejectedYesRejected before delivery, see the failure objectAll
fallback_deliveredYesRCS not possible; the SMS fallback was deliveredRich RCS
fallback_failedYesThe SMS fallback was attempted and failedRich RCS

How receipts are delivered (all channels)

QuickSMS delivers each delivery receipt as a webhook POSTed to your callback_url (per message) or your connection's default webhook URL. Rich RCS sends receive a message.status payload for each status transition (message.status is the technical webhook event name); Messaging API sends receive their DLR when the message reaches a final status. Every webhook carries the same headers:

HeaderValue
X-QuickSMS-TimestampUnix seconds when the webhook was sent; used in the signature and for replay protection.
X-QuickSMS-EventWhat the webhook carries, for example dlr for a Messaging API delivery receipt.
X-QuickSMS-Delivery-AttemptAttempt number, starting at 1; increments on each retry.
X-QuickSMS-Signaturesha256=<hex>, the HMAC described under signature verification.
Rich RCS webhook payload · message.status
{
  "api_version": "v2",
  "event": "message.status",
  "message_id": "msg_42a731415e521124",
  "customer_id": "cust_abc123",
  "channel": "RCS",
  "content_type": "rich_card",
  "status": "delivered",
  "fell_back_to_sms": false,
  "sent_at": "2026-07-14T10:11:01+00:00",
  "delivered_at": "2026-07-14T10:11:04+00:00",
  "read_at": null,
  "done_at": "2026-07-14T10:11:04+00:00",
  "metadata": { "order_id": "12345" }
}
  • Your customer_id and metadata are echoed on every event, correlate without a lookup.
  • A network.mcc_mnc object is included when mobile network information is available.
  • Read receipts arrive as a subsequent event with status: "read" and read_at populated.

Failure reasons

Receipts for failed, expired and rejected outcomes carry a failure object explaining why:

failure
"failure": {
  "reason": "not_capable",
  "description": "Destination not RCS capable"
}
failure.reasonMeaningWhat to do
not_capableThe recipient's device is not RCS-capableEnable SMS fallback, or send as SMS
not_reachableNo RCS route to the recipientEnable SMS fallback, or send as SMS
ttl_expiredValidity period elapsed before deliveryRetry with a longer expiry if still relevant
carrier_rejectedRejected by the recipient's mobile networkCheck sender registration and content compliance for the destination
delivery_rejectedRejected during downstream delivery, after QuickSMS accepted the messageUsually transient, retry; contact support with the message_id if it persists
content_unsupportedThe content could not be rendered or acceptedCheck media requirements and per-field limits
handset_unreachableThe device was offline for the whole validity periodRetry later, or rely on SMS fallback
unknownUnmapped failureContact support with the message_id, we hold the full diagnostic detail

Fallen-back messages report fell_back_to_sms: true with terminal status fallback_delivered or fallback_failed. Statuses and failure reasons are normalised by QuickSMS, you never need to understand downstream routing to interpret them.

Scope of failure.reason: it exists only in Rich RCS (message.status) payloads and supplements the platform's delivery model; Messaging API receipts carry GSM status codes instead, and nothing was added to or removed from them. RCS delivery never produces a GSM code. A GSM status_code (with message_parts) is present only when the message was actually delivered over SMS, including a Rich RCS message that fell back: that receipt carries the GSM code and the billed parts alongside the normalised status.

Signature verification

Every webhook is signed. The X-QuickSMS-Signature header carries sha256=<hex>, an HMAC-SHA256 of {timestamp}.{body} computed with your webhook signing secret (shown on your API connection); the timestamp is delivered in the X-QuickSMS-Timestamp header. Strip the sha256= prefix before comparing, and verify before trusting any receipt:

Verify a webhook
const crypto = require('crypto');

function verifyWebhook(rawBody, timestamp, signatureHeader, secret) {
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
    throw new Error('Timestamp too old');
  }
  const expected = crypto
    .createHmac('sha256', secret)
    .update(timestamp + '.' + rawBody)
    .digest('hex');
  const received = signatureHeader.replace(/^sha256=/, '');
  return crypto.timingSafeEqual(
    Buffer.from(expected, 'hex'),
    Buffer.from(received, 'hex')
  );
}
Verify a webhook
function verifyWebhook(string $rawBody, string $timestamp, string $signatureHeader, string $secret): bool
{
    if (abs(time() - (int) $timestamp) > 300) {
        return false; // timestamp too old
    }
    $expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
    $received = preg_replace('/^sha256=/', '', $signatureHeader);
    return hash_equals($expected, $received);
}
Verify a webhook
import hashlib
import hmac
import time

def verify_webhook(raw_body: bytes, timestamp: str, signature_header: str, secret: str) -> bool:
    if abs(time.time() - int(timestamp)) > 300:
        return False  # timestamp too old
    expected = hmac.new(
        secret.encode(),
        f"{timestamp}.".encode() + raw_body,
        hashlib.sha256,
    ).hexdigest()
    received = signature_header.removeprefix("sha256=")
    return hmac.compare_digest(expected, received)
Verify a webhook
using System.Security.Cryptography;
using System.Text;

static bool VerifyWebhook(string rawBody, string timestamp, string signatureHeader, string secret)
{
    var age = DateTimeOffset.UtcNow.ToUnixTimeSeconds() - long.Parse(timestamp);
    if (Math.Abs(age) > 300) return false; // timestamp too old

    using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
    var expected = Convert.ToHexString(
        hmac.ComputeHash(Encoding.UTF8.GetBytes($"{timestamp}.{rawBody}"))
    ).ToLowerInvariant();
    var received = signatureHeader.StartsWith("sha256=")
        ? signatureHeader["sha256=".Length..]
        : signatureHeader;
    return CryptographicOperations.FixedTimeEquals(
        Convert.FromHexString(expected), Convert.FromHexString(received));
}
Verify a webhook
static boolean verifyWebhook(String rawBody, String timestamp,
                             String signatureHeader, String secret) throws Exception {
    long age = Math.abs(System.currentTimeMillis() / 1000L - Long.parseLong(timestamp));
    if (age > 300) return false; // timestamp too old

    var mac = javax.crypto.Mac.getInstance("HmacSHA256");
    mac.init(new javax.crypto.spec.SecretKeySpec(
        secret.getBytes(java.nio.charset.StandardCharsets.UTF_8), "HmacSHA256"));
    byte[] expected = mac.doFinal(
        (timestamp + "." + rawBody).getBytes(java.nio.charset.StandardCharsets.UTF_8));

    String received = signatureHeader.startsWith("sha256=")
        ? signatureHeader.substring("sha256=".length())
        : signatureHeader;
    byte[] receivedBytes = java.util.HexFormat.of().parseHex(received);
    return java.security.MessageDigest.isEqual(expected, receivedBytes);
}
Verify a webhook
func verifyWebhook(rawBody []byte, timestamp, signatureHeader, secret string) bool {
    ts, err := strconv.ParseInt(timestamp, 10, 64)
    if err != nil || math.Abs(float64(time.Now().Unix()-ts)) > 300 {
        return false // timestamp too old
    }

    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write([]byte(timestamp + "."))
    mac.Write(rawBody)
    expected := hex.EncodeToString(mac.Sum(nil))

    received := strings.TrimPrefix(signatureHeader, "sha256=")
    return hmac.Equal([]byte(expected), []byte(received))
}
Verify a webhook
require "openssl"

def verify_webhook(raw_body, timestamp, signature_header, secret)
  return false if (Time.now.to_i - timestamp.to_i).abs > 300 # timestamp too old

  expected = OpenSSL::HMAC.hexdigest("SHA256", secret, "#{timestamp}.#{raw_body}")
  received = signature_header.delete_prefix("sha256=")
  OpenSSL.secure_compare(expected, received)
end
  • Compute the HMAC over the raw request body, parse the JSON only after verifying. Do not re-serialise the JSON; sign the exact bytes on the wire.
  • Strip the sha256= prefix from the header before the comparison, as in the examples.
  • Reject stale timestamps (older than 5 minutes) to prevent replay.
  • Use a constant-time comparison, as in the examples above.
  • The inbound webhook uses the same scheme with a per-number signing secret.

Delivery receipt retry schedule

Return any 2xx to acknowledge a webhook. A non-2xx response or a timeout counts as a failed attempt and is retried with backoff, with the attempt number in X-QuickSMS-Delivery-Attempt:

AttemptDelay after the previous failure
1Immediate
230 seconds
32 minutes
410 minutes
51 hour

After five failed attempts the webhook is dropped. The message's status is unaffected, only the notification is lost; reconcile with the portal (or, for Rich RCS sends, GET /v2/messages/{id}). This schedule applies to outbound delivery receipts from both endpoints. The inbound webhook is different: inbound failures are logged without automatic retries.

  • Respond fast with a 2xx. Acknowledge first, process asynchronously.
  • Expect duplicates. Retries mean the same receipt can arrive more than once. De-duplicate per contract: Messaging API receipts on (quicksms_id, message_status), Rich RCS receipts on (message_id, status).
  • Don't assume ordering. Use the payload timestamps (sent_at, delivered_at, read_at, done_at), not arrival order.
  • Webhooks beat polling, but GET /v2/messages/{id} is always available to reconcile.

Webhook and delivery receipt catalogue

EventFires whenChannelsAvailability
Messaging API delivery receipt (DLR) A message accepted by POST /v1/messages reaches a final status SMS, RCS text Available
message.status A message accepted by POST /v2/messages changes status (per transition) Rich RCS Available
Inbound message webhook An SMS reply arrives on a webhook-enabled inbound number. Configured per number, with its own signing secret; documented under Inbound webhook V2 SMS Available
Inbound RCS interactions RCS replies, reply suggestion taps and share_location results delivered through Inbound V2 RCS text, Rich RCS Coming soon
contact.opted_out A recipient opts out and the suppression record is created Contacts Coming soon

Every send endpoint links to the events it generates, and this catalogue links back to the operations that cause them, see POST /v2/messages.

Messaging & Contact Book APIs Updated 21 July 2026 · View changelog →

Errors & rate limits

QuickSMS APIs use a consistent error-envelope structure where supported: every non-2xx response gives you an HTTP status, a machine-readable code to branch on, a human-readable message for your logs, and a request_id for support. API-specific rejection codes and asynchronous delivery models are documented separately below.

Four separate vocabularies

Keep these four namespaces apart in your integration; a value from one never appears in another:

NamespaceWhere it appearsExamples
HTTP status Every response 401, 422, 429, see HTTP status codes
Synchronous error code error.code, returned on the API call itself, before anything is dispatched UNAUTHORIZED, SENDER_NOT_REGISTERED, OUT_OF_HOURS, see the catalogue below
Message lifecycle status Send responses, retrieval and delivery receipts Messaging API: uppercase ACCEPTED, DELIVERED, see the status vocabulary; Rich RCS endpoint: lowercase queued, delivered, read, see the lifecycle
Asynchronous delivery outcome Delivery receipts, after a message was accepted Messaging API: GSM status codes (0, 27, 255); Rich RCS payloads: failure.reason (not_capable, delivery_rejected), see failure reasons

The error envelope

Every error on the messaging and Rich RCS surface returns this envelope, including 404 (unknown path), 405 (wrong method), 400 for malformed JSON, and masked 500 responses:

Error response
{
  "error": {
    "type": "validation_error",
    "code": "VALIDATION_ERROR",
    "message": "The given data was invalid.",
    "request_id": "0dd0a4d6-d5b2-4df0-bcfa-93e4d95cb64c",
    "errors": {
      "mobile_number": ["The mobile number format is invalid."]
    }
  }
}
FieldDescription
typeLowercase semantic category (unauthorized, validation_error, rate_limit_exceeded, …). Stable, fall back to this when you don't recognise a specific code.
codeUPPERCASE machine-readable code. Branch on this, never on message.
messageHuman-readable summary for logs. Wording may change, don't parse it or show it to end users verbatim.
request_idUnique per request. Quote it to support so we can find the request in our logs.
errorsValidation failures only: per-field arrays of messages.
paramOptional, the request parameter or header the error refers to.
retry_after429 only: seconds to wait, also sent as the Retry-After header.

Request-level and recipient-level errors

Messaging sends distinguish two failure shapes. A request-level failure (bad credentials, malformed JSON, a missing field) returns the top-level error envelope above and no message record is created. A recipient-level rejection means the request was valid but the message itself was refused; it returns 422 with the rejection inside the messages array:

Recipient-level rejection · 422
{
  "total": 1,
  "accepted": 0,
  "rejected": 1,
  "messages": [{
    "mobile_number": "447700900123",
    "MSISDN": "447700900123",
    "status": "REJECTED",
    "error": {
      "code": "SENDER_NOT_REGISTERED",
      "message": "The sender ID is not approved on this account."
    }
  }]
}
  • total, accepted and rejected count the recipients in the request; today a send carries one recipient, so the message is either accepted or rejected. The shape is ready for batch sending, where one request will be able to contain both outcomes.
  • A rejected message gets no message ID and produces no delivery receipt; the synchronous response is the only record. The Rich RCS endpoint reports the same shape with mobile_number and lowercase rejected.
  • Rejections that identify an earlier message, such as DUPLICATE_MESSAGE, include the original message's ID in the response so you can correlate.

HTTP status codes

StatusMeaningWhen you'll see it
200 / 201OK / CreatedSuccessful reads, updates, deletes / resource creation.
202AcceptedQueued for asynchronous processing (message sends, by-filter bulk operations).
400Bad requestMalformed JSON, or an invalid Idempotency-Key.
401UnauthorizedMissing or invalid credentials; a Contact Book token that is suspended, revoked, expired or used from outside its permitted IP allowlist.
403ForbiddenInsufficient permission or scope; a Messaging connection accessed from an IP outside its allowlist (IP_NOT_ALLOWED).
404Not foundThe resource doesn't exist, or doesn't belong to your account.
409ConflictIdempotency key reused with a different body; by-filter count drift.
422Validation failedThe request was well-formed but a field is invalid, check errors.
429Rate limitedToo many requests, wait retry_after seconds, then retry.
500Server errorOur side. Safe to retry with exponential backoff.

Request-level error codes

Codes are stable, new ones may be added, so fall back to type for anything unrecognised. Codes marked Contact Book or Messaging are specific to that surface; the rest apply everywhere.

CodeHTTPLikely causeHow to resolveRetry?
UNAUTHORIZED401 Bad or inactive credential; for Contact Book tokens, also an IP outside the allowlist Check the key in your Authorization header and its state in API Connections Fix first
FORBIDDEN403 Missing scope, the response echoes required_scope and your token's available_scopes Create a credential with the required scope Fix first
IP_NOT_ALLOWED403 A Messaging API connection was used from an IP outside its allowlist Add the caller's IP to the connection's allowlist, or call from a permitted address Fix first
NOT_FOUND404 Wrong ID, or the resource belongs to another account Check the identifier; IDs are account-scoped Fix first
VALIDATION_ERROR422 A field failed validation, e.g. a mobile number in the wrong format for the API you are calling (Messaging: digits only, 447700900123; Contact Book: E.164, +447700900123) Fix the fields listed in errors Fix first
MISSING_FIELD400 Messaging: a required field is absent; param names it Add the missing field Fix first
BAD_REQUEST400 Messaging: the request body is not valid JSON Validate the JSON format Fix first
INVALID_PHONE_NUMBER422 Messaging: the mobile number is not valid international format Digits only, no +, 7 to 15 digits, country code first Fix first
ACCOUNT_INACTIVE403 Messaging: the account or connection cannot send Check the connection state in API Connections, or contact support Fix first
AUTH_RATE_LIMITED429 Repeated failed authentication attempts triggered the brute-force lockout Wait the Retry-After interval and verify the key is correct before retrying Yes, after waiting
INVALID_CONTENT422 The content object failed structural validation (limits, missing fields, bad URLs) Check message and param for the failing field; see content types Fix first
CONTENT_TOO_LONG422 A text field exceeds its limit See per-field limits under content types Fix first
IDEMPOTENCY_CONFLICT409 Messaging: the same Idempotency-Key was sent with a different request body Generate a fresh key per distinct operation Fix first
IDEMPOTENCY_KEY_REUSED409 Contact Book: the same Idempotency-Key was sent with a different request body. The two surfaces use different codes for this conflict today Generate a fresh key per distinct operation Fix first
INVALID_IDEMPOTENCY_KEY400 Empty or over-long key Use up to 128 ASCII characters, a UUID works well Fix first
CONFLICT409 By-filter expected_count drifted beyond tolerance Refresh the count and retry Yes, after refresh
RATE_LIMITED429 Too many requests in the window Wait retry_after seconds, then retry Yes, after waiting
INTERNAL_ERROR500 Something went wrong on our side Retry with exponential backoff; contact support with the request_id if it persists Yes, with backoff

Messaging rejection codes

These codes appear at the recipient level, inside messages[].error on a 422 response, when a valid request was refused for a specific message. No message ID is issued and no delivery receipt follows.

CodeMeaningHow to resolve
SENDER_NOT_REGISTERED The sender ID is not approved on the account Register the sender ID and wait for approval
SENDER_NOT_RCS_AGENT Rich content or smart_routing with a sender that isn't an approved RCS agent Use your agent name, or register an agent
TEST_NUMBER_NOT_APPROVED Test mode: the recipient is not in your test allowlist Add the number to the allowlist in API Connections
COUNTRY_NOT_ALLOWED The destination country is blocked for your account, see country restrictions Check your destination settings, or contact support to enable the country
INSUFFICIENT_CREDITS Account balance is too low for the send Top up the account
MESSAGE_REJECTED Pending deploy The message was rejected at submission. The reason is deliberately generic in the API response; the specific reason (for example a content-filter rule) is visible to the account owner in the portal message logs Open the message in the portal to see why it was rejected, then revise and resend
OUT_OF_HOURS Submitted outside the account's sending window with reject mode active, see out-of-hours protection Retry during the window, or switch the account to hold mode
DUPLICATE_MESSAGE Anti-flood detected identical content to the same recipient within the window; the response identifies the original message, see anti-flood protection Wait for the window to pass, or change the content if it is genuinely a new message

Errors by channel

Which codes and delivery-failure vocabularies apply to the channel you are sending on:

Compare messaging channels →
StageWhat you can receive on SMS
Rejection codes (422, recipient-level)SENDER_NOT_REGISTERED, TEST_NUMBER_NOT_APPROVED, COUNTRY_NOT_ALLOWED, INSUFFICIENT_CREDITS, MESSAGE_REJECTED, OUT_OF_HOURS, DUPLICATE_MESSAGE
Request-level codesINVALID_PHONE_NUMBER, CONTENT_TOO_LONG (over 1,530 chars), MISSING_FIELD, BAD_REQUEST, plus the shared auth and rate-limit codes above
Idempotency conflictIDEMPOTENCY_CONFLICT (409)
Delivery failures (asynchronous)GSM status codes in the receipt: 1, 13, 27, 32, 255, see GSM status codes. No failure.reason vocabulary applies
StageWhat you can receive on RCS text
Rejection codes (422, recipient-level)Everything SMS can return, plus SENDER_NOT_RCS_AGENT when smart_routing is enabled with a sender that isn't an approved RCS agent
Request-level codesSame as SMS: INVALID_PHONE_NUMBER, CONTENT_TOO_LONG, MISSING_FIELD, BAD_REQUEST, plus the shared codes above
Idempotency conflictIDEMPOTENCY_CONFLICT (409)
Delivery failures (asynchronous)GSM status codes when the message was delivered (or failed) over the SMS fallback; RCS-delivered messages report through the same uppercase receipt statuses with the read-receipt fields
StageWhat you can receive on Rich RCS
Rejection codes (422, recipient-level)SENDER_NOT_RCS_AGENT, TEST_NUMBER_NOT_APPROVED, COUNTRY_NOT_ALLOWED, INSUFFICIENT_CREDITS, MESSAGE_REJECTED, OUT_OF_HOURS, DUPLICATE_MESSAGE
Request-level codesINVALID_CONTENT (structural validation of the content object), CONTENT_TOO_LONG (per-field limits: title 200, description 2,000, labels 25, sms_body 1,530), VALIDATION_ERROR for mobile_number format (including MSISDN sent by mistake), plus the shared codes above
Idempotency conflictIDEMPOTENCY_CONFLICT (409)
Delivery failures (asynchronous)failure.reason on message.status receipts: not_capable, not_reachable, ttl_expired, carrier_rejected, delivery_rejected, content_unsupported, handset_unreachable, unknown, see failure reasons. when a Rich RCS message falls back to SMS, the receipt carries the GSM status_code and message_parts alongside the normalised status; RCS delivery itself never produces a GSM code

Synchronous vs asynchronous failures. This page covers errors returned directly on the API call. A message that is accepted but later fails to deliver is reported through delivery receipts: GSM status codes for Messaging API sends, failure.reason for Rich RCS sends, both equally stable.

Rate limits

Limits are per credential, per minute, on a rolling 60-second window.

SurfaceTierLimit
Messaging APIStandard1,000 requests / minute by default, raisable per connection. Rate-limit headers on every response; use the returned limit rather than hard-coding the number.
Contact Book APIStandard300 requests / minute.
Contact Book APIBulk import endpoints60 requests / minute.

The messaging API reports your live position on every response:

HeaderDescription
X-RateLimit-LimitMaximum requests per minute for this connection.
X-RateLimit-RemainingRequests remaining in the current window.
X-RateLimit-ResetUnix timestamp when the window resets.
Retry-AfterOn 429 only: seconds to wait before retrying.

Monitor X-RateLimit-Remaining and throttle your own sending when it falls below roughly 10% of your limit, then ramp back up gradually after the reset. At the limit you receive 429 with Retry-After (header and body). Aggressive retry loops without backoff keep you at the limit indefinitely, always wait the indicated interval. Need more? Contact support with your account ID and workload; per-credential limits can be raised.

Retrying API requests

Not every error should be retried. Decide from the HTTP status:

StatusRetry?Strategy
429YesWait the Retry-After value, then retry.
500YesRetry with exponential backoff.
503YesService degraded; retry with backoff.
400 to 422NoThe request is the problem; fix it before resending.

Recommended backoff, with jitter so concurrent clients don't retry in lockstep:

AttemptBase delayWith jitter
11 second0.8 to 1.2 s
22 seconds1.6 to 2.4 s
34 seconds3.2 to 4.8 s
48 seconds6.4 to 9.6 s
516 seconds12.8 to 19.2 s

Stop after five attempts and log the failure with its request_id. Always send the same Idempotency-Key on every retry of a send, so a request that actually landed is replayed rather than duplicated:

Send with retries
const RETRYABLE = new Set([429, 500, 503]);

async function sendWithRetries(payload, idempotencyKey, attempts = 5) {
  for (let attempt = 1; attempt <= attempts; attempt++) {
    const response = await fetch('https://api.quicksms.com/v1/messages', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.QUICKSMS_API_KEY}`,
        'Content-Type': 'application/json',
        'Idempotency-Key': idempotencyKey, // same key on every attempt
      },
      body: JSON.stringify(payload),
      signal: AbortSignal.timeout(15000),
    });

    if (response.ok) return response.json();

    const body = await response.json().catch(() => ({}));
    if (!RETRYABLE.has(response.status)) {
      // 400-422: fix the request, don't retry
      throw new Error(`${body.error?.code}: ${body.error?.message} (request_id ${body.error?.request_id})`);
    }

    const retryAfter = Number(response.headers.get('Retry-After'));
    const backoff = retryAfter ? retryAfter * 1000
      : 1000 * 2 ** (attempt - 1) * (0.8 + Math.random() * 0.4);
    console.warn(`Attempt ${attempt} got ${response.status}, request_id ${body.error?.request_id}, waiting ${Math.round(backoff)}ms`);
    await new Promise(resolve => setTimeout(resolve, backoff));
  }
  throw new Error('Send failed after 5 attempts');
}
Send with retries
import os, random, time, requests

RETRYABLE = {429, 500, 503}

def send_with_retries(payload, idempotency_key, attempts=5):
    for attempt in range(1, attempts + 1):
        response = requests.post(
            'https://api.quicksms.com/v1/messages',
            headers={
                'Authorization': f"Bearer {os.environ['QUICKSMS_API_KEY']}",
                'Idempotency-Key': idempotency_key,  # same key on every attempt
            },
            json=payload,
            timeout=15,
        )
        if response.ok:
            return response.json()

        error = response.json().get('error', {})
        if response.status_code not in RETRYABLE:
            # 400-422: fix the request, don't retry
            raise RuntimeError(f"{error.get('code')}: {error.get('message')} (request_id {error.get('request_id')})")

        retry_after = response.headers.get('Retry-After')
        backoff = (float(retry_after) if retry_after
                   else 2 ** (attempt - 1) * random.uniform(0.8, 1.2))
        print(f"Attempt {attempt} got {response.status_code}, request_id {error.get('request_id')}, waiting {backoff:.1f}s")
        time.sleep(backoff)
    raise RuntimeError('Send failed after 5 attempts')
Send with retries
const RETRYABLE = [429, 500, 503];

function sendWithRetries(array $payload, string $idempotencyKey, int $attempts = 5): array
{
    for ($attempt = 1; $attempt <= $attempts; $attempt++) {
        $ch = curl_init('https://api.quicksms.com/v1/messages');
        curl_setopt_array($ch, [
            CURLOPT_POST           => true,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => 15,
            CURLOPT_HTTPHEADER     => [
                'Authorization: Bearer ' . getenv('QUICKSMS_API_KEY'),
                'Content-Type: application/json',
                'Idempotency-Key: ' . $idempotencyKey, // same key on every attempt
            ],
            CURLOPT_POSTFIELDS => json_encode($payload),
            CURLOPT_HEADERFUNCTION => function ($ch, $header) use (&$retryAfter) {
                if (stripos($header, 'Retry-After:') === 0) {
                    $retryAfter = (int) trim(substr($header, 12));
                }
                return strlen($header);
            },
        ]);
        $retryAfter = null;
        $body   = curl_exec($ch);
        $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        $data = json_decode($body, true) ?? [];

        if ($status >= 200 && $status < 300) {
            return $data;
        }
        if (!in_array($status, RETRYABLE, true)) {
            // 400-422: fix the request, don't retry
            throw new RuntimeException(sprintf('%s: %s (request_id %s)',
                $data['error']['code'] ?? $status,
                $data['error']['message'] ?? 'request failed',
                $data['error']['request_id'] ?? 'n/a'));
        }
        $backoff = $retryAfter ?? (2 ** ($attempt - 1)) * (mt_rand(80, 120) / 100);
        error_log(sprintf('Attempt %d got %d, request_id %s, waiting %.1fs',
            $attempt, $status, $data['error']['request_id'] ?? 'n/a', $backoff));
        usleep((int) ($backoff * 1_000_000));
    }
    throw new RuntimeException('Send failed after 5 attempts');
}
Send with retries
static readonly int[] Retryable = { 429, 500, 503 };

static async Task<string> SendWithRetries(object payload, string idempotencyKey, int attempts = 5)
{
    using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(15) };
    client.DefaultRequestHeaders.Add("Authorization",
        $"Bearer {Environment.GetEnvironmentVariable("QUICKSMS_API_KEY")}");
    client.DefaultRequestHeaders.Add("Idempotency-Key", idempotencyKey); // same key on every attempt

    for (var attempt = 1; attempt <= attempts; attempt++)
    {
        var response = await client.PostAsJsonAsync(
            "https://api.quicksms.com/v1/messages", payload);
        var body = await response.Content.ReadAsStringAsync();
        if (response.IsSuccessStatusCode) return body;

        var status = (int)response.StatusCode;
        if (Array.IndexOf(Retryable, status) < 0)
        {
            // 400-422: fix the request, don't retry
            throw new HttpRequestException($"{status}: {body}");
        }
        var retryAfter = response.Headers.RetryAfter?.Delta
            ?? TimeSpan.FromSeconds(Math.Pow(2, attempt - 1) * (0.8 + Random.Shared.NextDouble() * 0.4));
        Console.Error.WriteLine($"Attempt {attempt} got {status}, waiting {retryAfter.TotalSeconds:F1}s; body: {body}");
        await Task.Delay(retryAfter);
    }
    throw new HttpRequestException("Send failed after 5 attempts");
}
Send with retries
static final java.util.Set<Integer> RETRYABLE = java.util.Set.of(429, 500, 503);

static String sendWithRetries(String jsonPayload, String idempotencyKey, int attempts) throws Exception {
    var client = java.net.http.HttpClient.newBuilder()
        .connectTimeout(java.time.Duration.ofSeconds(15)).build();
    var request = java.net.http.HttpRequest.newBuilder()
        .uri(java.net.URI.create("https://api.quicksms.com/v1/messages"))
        .timeout(java.time.Duration.ofSeconds(15))
        .header("Authorization", "Bearer " + System.getenv("QUICKSMS_API_KEY"))
        .header("Content-Type", "application/json")
        .header("Idempotency-Key", idempotencyKey) // same key on every attempt
        .POST(java.net.http.HttpRequest.BodyPublishers.ofString(jsonPayload))
        .build();

    for (int attempt = 1; attempt <= attempts; attempt++) {
        var response = client.send(request, java.net.http.HttpResponse.BodyHandlers.ofString());
        if (response.statusCode() / 100 == 2) return response.body();
        if (!RETRYABLE.contains(response.statusCode())) {
            // 400-422: fix the request, don't retry
            throw new RuntimeException(response.statusCode() + ": " + response.body());
        }
        long backoffMs = response.headers().firstValue("Retry-After")
            .map(v -> Long.parseLong(v) * 1000L)
            .orElse((long) (Math.pow(2, attempt - 1) * (800 + java.util.concurrent.ThreadLocalRandom.current().nextInt(400))));
        System.err.printf("Attempt %d got %d, waiting %dms; body: %s%n",
            attempt, response.statusCode(), backoffMs, response.body());
        Thread.sleep(backoffMs);
    }
    throw new RuntimeException("Send failed after 5 attempts");
}

Idempotency

Network failures leave you unsure whether a write landed. Send an Idempotency-Key header on state-changing requests and retries become safe: a duplicate within 24 hours replays the original response byte-for-byte (flagged with an Idempotent-Replay: true header) instead of repeating the work.

  • Key: up to 128 ASCII characters, unique per logical operation. Tie keys to your business event ("order-12345-dispatch") rather than random UUIDs, so retries naturally reuse the same key.
  • Scope: per credential; different credentials can use the same key without collision. Test and live are separate credentials, so their key spaces never collide.
  • TTL: 24 hours from first use.
  • Same key, same body: the original response is replayed instead of repeating the work; the Contact Book flags replays with an Idempotent-Replay: true header, messaging returns the cached 202.
  • Same key, different body: 409, protecting you from racing two different operations under one key. Messaging returns IDEMPOTENCY_CONFLICT; the Contact Book returns IDEMPOTENCY_KEY_REUSED.
  • Reads: keys are ignored on GET requests. A key longer than 128 characters is rejected with a validation error.

All API surfaces Updated 21 July 2026 · View changelog →

Security best practices

Everything security-related in one checklist: credentials, webhooks, retries and logging. Each item links to the page that documents the mechanism in detail.

Credentials

PracticeDetail
Store keys in a secrets manager Environment variables or a secrets manager, never source control. Credentials are shown once at creation, see Authentication.
Keep test and live separate CI, development and staging hold sk_test_ / cbk_test_ credentials only; reserve live keys for production.
One credential per integration Easier to audit and revoke individually; a leak takes out one integration, not all of them.
Use the narrowest scopes Contact Book tokens carry per-resource scopes; a read-only dashboard needs contacts:read and nothing else.
Rotate compromised keys immediately Create the replacement, switch your integration over, then revoke the old credential in API Connections.
Enable production IP allowlisting Restrict each credential to your servers' IPs. A blocked messaging request returns 403 IP_NOT_ALLOWED; a blocked Contact Book request returns 401, see credential lifecycle.

Webhooks

PracticeDetail
HTTPS callback URLs only Configure every webhook endpoint over HTTPS; the inbound webhook refuses plain HTTP URLs outright.
Verify every signature Validate X-QuickSMS-Signature (sha256=<hex>, HMAC-SHA256 over {timestamp}.{body}) before processing anything, see signature verification.
Sign over the raw bytes Verify against the exact request body on the wire; re-serialising the JSON changes the bytes and breaks the comparison.
Reject stale timestamps Refuse webhooks whose X-QuickSMS-Timestamp is more than 5 minutes old, blocking replay attacks.
Compare in constant time Use your language's constant-time comparison (timingSafeEqual, hash_equals, hmac.compare_digest), never ==.
Protect signing secrets separately Webhook signing secrets are not API keys; store them under their own secret names so rotating one never touches the other.
Rotate signing secrets deliberately Rotation is immediate: the old secret stops working the moment a new one is generated, so update your verifier first, then rotate.

Requests and retries

PracticeDetail
Use idempotency keys on sends An Idempotency-Key makes network retries safe; reuse the same key on every retry of the same operation, see Idempotency.
Retry only what's retryable 429, 500 and 503 with backoff and jitter; never blind-retry 400 to 422, see retrying API requests.
Log the request_id Every error carries one; logging it turns a support conversation from guesswork into a lookup.
Never log full credentials Redact the Authorization header and key values in request logs, crash reports and error trackers.

All API surfaces Updated 21 July 2026 · View changelog →

Troubleshooting & FAQ

Fix integration problems by the symptom you can see. Each section gives the most likely causes in order, the fix, and a link to the full explanation; the quick answers at the end cover the decisions developers ask about most. Nothing here replaces the reference pages, it routes you to them faster.

Start here: the two-stage check

Every send has two independently checkable stages, and most confusion comes from conflating them. A 202 Accepted means QuickSMS accepted the request; it is not proof of delivery. Delivery is reported asynchronously by the delivery receipt.

  1. Check the synchronous response. A non-2xx status or a REJECTED entry in messages[] means the request itself failed; branch on the error.code using the rejection code catalogue. A rejected message gets no ID and no receipt.
  2. Then check the delivery receipt. An accepted message reaches a final status (DELIVERED, UNDELIVERED, EXPIRED, REJECTED) with a GSM status code on the Messaging API, or a lowercase lifecycle status with failure.reason on the Rich RCS API. PENDING is not final.

Four separate code vocabularies exist and a value from one never appears in another; if a code looks unfamiliar, first identify which vocabulary you are looking at.

My API call is rejected

401 UNAUTHORIZED or AUTH_RATE_LIMITED

  • Wrong credential family: messaging endpoints need an sk_ key, Contact Book endpoints need a cbk_ token. They are not interchangeable.
  • Using Basic auth: the key is the username with a blank password, and the trailing colon before base64 encoding is required (base64(api_key:)).
  • AUTH_RATE_LIMITED: 10 failed attempts from one IP within 60 seconds locks that IP for 5 minutes; wait for the Retry-After header rather than retrying immediately.

Full rules: Authentication.

403 IP_NOT_ALLOWED

Your connection has an IP allowlist and this request came from an address not on it. Note the asymmetry: messaging returns 403 IP_NOT_ALLOWED, while a Contact Book token outside its allowlist returns 401. Update the allowlist on the connection, or check which egress IP your servers actually use.

SENDER_NOT_REGISTERED or SENDER_NOT_RCS_AGENT

  • Alphanumeric sender IDs: maximum 11 characters and must contain at least one letter. Numeric senders: maximum 15 digits. Anything else fails validation before registration is even checked.
  • The sender must be registered, approved, and valid for the destination country.
  • SENDER_NOT_RCS_AGENT: smart_routing and all rich content require your approved RCS agent name as the sender; an SMS sender ID there is rejected synchronously.

Formats and validation: sender formats.

VALIDATION_ERROR on the recipient number

  • Messaging endpoints take digits only, with country code, no +, 7 to 15 digits (447700900123). The Contact Book API differs: E.164 with a leading +.
  • On POST /v1/messages, send mobile_number (preferred) or MSISDN (alias), not both with different values. On POST /v2/messages, only mobile_number is accepted.

The full cross-API mapping: field names across the two send APIs.

TEST_NUMBER_NOT_APPROVED

Test keys can only send to numbers on the connection's approved test allowlist. Add the recipient in the portal, or switch to a live key if this is a production send. See test and live environments.

My message was accepted but never arrived

  • You are in sandbox mode. A sk_test_ key (or "sandbox": true) never delivers to a handset; receipts are simulated after a few seconds. This is the single most common cause. Check the key prefix and the "sandbox" field in the response.
  • Deliberate test outcomes: in sandbox, a recipient number ending 0000 simulates UNDELIVERED (GSM 27) and 9999 simulates EXPIRED (GSM 255).
  • The message is HELD: a sending control such as an out-of-hours window is holding it, with scheduled_send_at in the response. Credits are reserved; it sends when the window opens. See sending controls.
  • It expired: delivery is attempted for expiry seconds (default 48 hours), then the message ends EXPIRED with a failure receipt, commonly when the handset is off (GSM 27, absent subscriber).
  • The receipt is the truth: look up the final status and GSM code rather than assuming; PENDING means not final yet.

My message cost more parts than expected

  • GSM-7 texts fit 160 characters in one part (153 per part when concatenated). One character outside GSM-7, including curly quotes or an emoji pasted from a document, silently switches the whole message to UCS-2: 70 characters per part (67 concatenated).
  • GSM-7 extension characters (such as , [, ], ~) count as two characters each.
  • The response tells you what happened: check encoding and message_parts; you are billed on message_parts.

Limits and the encoding table: content length limits. The request builder shows encoding and parts live as you type.

My RCS message arrived as SMS

  • That is smart routing working, not failing: the recipient's handset or route was not RCS-reachable, so QuickSMS applied your configured SMS fallback. The receipt's channel field reports what was actually delivered.
  • If the fallback sender was not what you expected, it was resolved through the four-step priority chain; set an account default sender or pass an explicit fallback sender to control it.
  • Billing follows the delivered channel: an RCS-delivered text bills as RCS Basic or RCS Single by length; a fallback SMS bills independently on its own encoding and parts.
  • On the Rich RCS API there is no silent downgrade: rich content to a non-RCS recipient uses your explicit fallback object, or fails with a failure.reason if fallback is not enabled.

My webhook signature verification fails

  • Wrong base string. Delivery receipts and Inbound V2 SMS sign {timestamp}.{body}; the RCS agent inbound webhook signs the raw body only. Verifying one webhook with the other's base always fails.
  • Parsed-then-reserialised body. Compute the HMAC over the raw request bytes before any JSON parsing; frameworks that re-encode the body change the bytes and break the digest.
  • The prefix. Strip sha256= from the header value before comparing, and compare in constant time.
  • Rotated secret. Rotating a signing secret invalidates the old one immediately; make sure the receiver was updated.

Worked verification code in four languages: signature verification.

I'm not receiving webhooks or inbound messages

  • Webhook URLs must be HTTPS; plain HTTP is not accepted.
  • Check which URL actually applies: a per-message callback_url overrides the connection default.
  • For replies: an alphanumeric sender cannot receive them, send from an inbound number. For RCS replies: the agent must be Live and have inbound configuration enabled.
  • Design for the transport's guarantees: expect occasional duplicates (de-duplicate on the message ID) and do not assume ordering.
  • Enable a second destination (the portal Inbox or email forwarding) as a safety net while diagnosing, so messages are not lost if your endpoint is down.

My rich card or carousel is rejected

  • Per-field limits: title 200, card description 2,000, button and chip labels 25, fallback sms_body 1,530 (CONTENT_TOO_LONG names the field).
  • Media URLs must be HTTPS, publicly reachable without authentication, served with an image content type, and must not redirect; see media requirements.
  • A rich card needs at least one of title, description or media; horizontal cards require media.
  • The sender must be your approved RCS agent, not an SMS sender ID.

The request builder validates all of this before you copy a request.

Quick answers

Which API do I use, /v1 or /v2?

SMS and RCS text: the Messaging API, POST /v1/messages. Cards, carousels and media: the Rich RCS API, POST /v2/messages, which also accepts plain text if you want the structured content model for a new integration. Neither replaces the other; see choose a channel.

Which credential do I need?

Messaging (SMS, RCS text, Rich RCS): an API key, sk_live_ or sk_test_. Contact Book: a scoped token, cbk_. They are separate on purpose: a leaked messaging key cannot read your contacts, and vice versa. See Authentication.

Why does my live RCS send not reach my phone, when sandbox "works"?

Two different test concepts. The API sandbox (sk_test_) never delivers to any handset and simulates receipts. Google's RCS agent test phase is separate: a live (sk_live_) RCS send from an agent still in verification only reaches handsets registered as testers on that agent. See test and live environments.

How long does RCS agent verification take?

Registration includes verification with Google and typically takes 1 to 2 weeks. While the agent is in its test phase, receiving handsets must be added as testers. See Rich RCS.

What happens when the recipient's phone doesn't support RCS?

With smart_routing on the Messaging API, QuickSMS delivers the text as SMS automatically. On the Rich RCS API, your explicit fallback object supplies the SMS rendition; without it the message fails with a failure.reason.

If my RCS message falls back to SMS, what am I billed?

Billing follows the channel actually delivered: RCS text by length (RCS Basic up to 160 characters, RCS Single from 161), or the fallback SMS on its own encoding and parts. They are alternative outcomes, never two charges for one delivery. Wallet reservation detail is in your commercial terms.

How do I retry a failed request safely?

Send an Idempotency-Key header and reuse the same key on retries; it is cached for 24 hours, so a retried send never delivers twice. Respect Retry-After on 429. The full production pattern is in retrying API requests.

Should I use mobile_number or MSISDN?

Use mobile_number everywhere; it works on both send APIs and is echoed in responses. MSISDN remains fully supported on /v1/messages as an alias for existing integrations, with no deprecation. Never send both with different values.

Contacting support

So the first reply can be the fix, include:

  • The request_id from the error envelope (every non-2xx response carries one), or the message ID (quicksms_id / message_id) for delivery questions.
  • The endpoint, the environment (test or live), and the timestamp with timezone.
  • For delivery issues: the recipient number and the receipt you received (or that none arrived).
  • For webhook issues: the raw request body and headers exactly as received.

Never include a full API key or signing secret in a support message; the key prefix and the connection name identify it.

Messaging API & Rich RCS Updated 21 July 2026 · View changelog →

API reference

Field-by-field reference for every endpoint. Messaging and Rich RCS endpoints authenticate with a Messaging API key (sk_…); Contact Book endpoints with a scoped token (cbk_…), see which credential you need. URL paths contain version numbers for historical reasons; they do not indicate product generations.

SurfaceAddressCredential
Messaging API (SMS, RCS text)POST https://api.quicksms.com/v1/messagessk_live_ / sk_test_
QuickSMS Rich RCS API (opt-in)POST https://api.quicksms.com/v2/messagessk_live_ / sk_test_
Contact Book (separate product)https://api.quicksms.com/v1cbk_ / cbk_test_

Field names across the two send APIs

The Messaging API and the Rich RCS API describe the same concepts with different field names and status vocabularies. If you integrate both, this is the mapping. The recipient field has converged on mobile_number in both the request and the send response, but the other names still differ per surface.

ConceptMessaging API (/v1/messages)Rich RCS API (/v2/messages)
Recipient (request)mobile_number (or MSISDN, alias)mobile_number
Recipient (in the send response)mobile_number (with MSISDN as a compatibility duplicate)to
Message identifierquicksms_idmessage_id
Message textmsgcontent.text (structured content object)
Send status vocabularyUPPERCASE (ACCEPTED, DELIVERED, REJECTED)lowercase (queued, delivered, read, failed)
Units in the responsemessage_parts (SMS segments)cost (per-message)
Delivery outcome detailGSM status codesfailure.reason taxonomy

Messaging API

POST /v1/messages SMS · RCS Text

Send a message

The current production send endpoint: mobile_number (or MSISDN), msg and sender, with smart_routing for RCS text. The complete contract follows.

Full Messaging API contract

The complete contract for the current Messaging API. It shares idempotency, rate limits and sending controls with the Rich RCS endpoint; what is specific here is the request shape, the uppercase status vocabulary and the delivery receipt (DLR) payload.

Request

FieldRequiredDescription
mobile_numberRequiredThe recipient's mobile number. International format, digits only, no + prefix, 7 to 15 digits. Example: 447700900123. MSISDN is accepted as a fully supported alias for existing integrations; send one or the other, and if both are present with different values the request is rejected.
msgRequiredMessage text, 1 to 1,530 characters.
senderRequiredApproved sender ID, or your RCS agent name when smart_routing is enabled.
smart_routingOptionalDeliver via RCS with automatic SMS fallback. Default false. Requires an approved RCS agent as the sender.
sms_failover_senderOptionalSMS sender ID to use if an RCS message falls back to SMS, see the fallback sender resolution order.
expiryOptionalValidity in seconds. Min 60, max 259,200 (72 h), default 172,800 (48 h).
customer_idOptionalYour reference, max 128 chars. Echoed in DLR webhooks.
metadataOptionalKey-value pairs, max 10 keys, values max 256 chars. Echoed in DLR webhooks.
sandboxOptionalForce sandbox mode on a live key. Default false.
callback_urlOptionalPer-message DLR webhook URL, max 2,048 chars.
Send · response (202 Accepted)
{
  "sandbox": false,
  "total": 1,
  "accepted": 1,
  "rejected": 0,
  "messages": [{
    "quicksms_id": "msg_80fac5c752941b27",
    "mobile_number": "447700900123",
    "MSISDN": "447700900123",
    "sender": "YOUR_SENDER_ID",
    "status": "ACCEPTED",
    "channel": "SMS",
    "encoding": "GSM-7",
    "message_parts": 1,
    "customer_id": "cust_abc123"
  }],
  "created_at": "2026-03-26T12:00:00+00:00"
}

encoding is GSM-7 or UCS-2 (null for RCS), and message_parts is the SMS segment count (always 1 for RCS). A rejected message returns 422 with status: "REJECTED" and an error object (code and message), see the messaging rejection codes.

Note: the response echoes the recipient as mobile_number, matching the request field. MSISDN is retained as a duplicate key with the same value so existing response parsers keep working; read mobile_number in new integrations.

Status vocabulary

This endpoint uses uppercase statuses. In the send response:

StatusMeaningNext step
ACCEPTEDQueued for deliveryWait for the DLR webhook
HELDDelayed by an out-of-hours ruleDispatched automatically at scheduled_send_at; credits are reserved immediately
REJECTEDFailed validation, never sentCheck error.code and fix; no webhook follows

And in delivery receipts:

StatusFinal?Meaning
PENDINGNoAwaiting carrier confirmation
DELIVEREDYesConfirmed delivered to the handset
UNDELIVEREDYesDelivery failed, for example the phone was off
EXPIREDYesValidity period elapsed before delivery
REJECTEDYesRejected by the carrier

Delivery receipt (DLR) webhook

When a message reaches a final status, a POST is made to your callback URL with the same signed transport as the Rich RCS endpoint's payloads, only the shape differs:

DLR · headers and payload
POST {your_callback_url}
Content-Type: application/json
X-QuickSMS-Timestamp: 1711468800
X-QuickSMS-Event: dlr
X-QuickSMS-Delivery-Attempt: 1
X-QuickSMS-Signature: sha256=5d7a8c3e...

{
  "quicksms_id": "msg_80fac5c752941b27",
  "customer_id": "cust_abc123",
  "channel": "SMS",
  "message_status": "DELIVERED",
  "status_code": 0,
  "message_parts": 1,
  "message_sent_time": "2026-03-26T12:00:01+00:00",
  "delivery_receipt_time": "2026-03-26T12:00:03+00:00",
  "read_receipt": null,
  "read_receipt_timestamp": null,
  "done_time": "2026-03-26T12:00:03+00:00",
  "metadata": { "order_id": "12345" }
}
FieldTypeDescription
quicksms_idstringThe message ID from the send response.
customer_idstring or nullYour reference, if provided.
channelstringThe channel actually used, SMS or RCS. May differ from the original request if RCS fell back to SMS.
message_statusstringFinal delivery status from the table above.
status_codeintegerGSM status code, see below.
message_partsintegerSegment count.
read_receiptboolean or nulltrue if read (RCS only), null for SMS.
read_receipt_timestampstring or nullISO 8601 read time (RCS only).
done_timestringISO 8601 time the message reached its final status.
metadataobject or nullYour metadata, if provided.

Return any 2xx to acknowledge; failed deliveries are retried on the shared webhook retry schedule, and signature verification is identical across all QuickSMS webhooks.

GSM status codes

CodeMeaning
0Delivered successfully
1Invalid destination number
13Rejected by mobile network
27Absent subscriber (phone off or out of range)
32Network error
255Unknown or expired

These codes are part of the current SMS delivery model, they are not legacy.

GET /v1/health

Service status

No authentication. 200 with {"status": "healthy"}, or 503 with {"status": "degraded"} (retry with backoff). Also available at /v2/health and /health; use any for uptime monitoring.

Rich RCS API

A first-class peer to the Messaging API, opt-in, for rich content. Rich RCS is opt-in on a separate endpoint; every existing integration continues to work with zero changes.

POST /v2/messages Rich RCS · structured content

Send a message

Validates, prices and queues a message to a single recipient in one request, using the structured content model. The recipient field on this endpoint is mobile_number; MSISDN is not accepted here and returns a validation error pointing to mobile_number.

Headers

HeaderRequiredDescription
AuthorizationRequiredBearer sk_…
Content-TypeRequiredapplication/json
Idempotency-KeyOptionalMakes retries safe; cached 24 hours. Details.

Request body

FieldTypeRequiredDescription
mobile_numberstringRequiredThe recipient's mobile number in international format, using digits only. Include the country code and omit the leading +, spaces and punctuation. Example: 447700900123. The Contact Book API differs: it stores and returns numbers in E.164 with a leading +.
senderstringRequiredSMS: an approved SMS sender ID. RCS text (smart_routing) and rich content: your approved RCS agent name (max 25 chars).
contentobjectRequiredA type (text | media | rich_card | carousel) plus the matching sub-object. Content types.
fallbackobjectOptionalSMS fallback behaviour for rich content. Fallback.
smart_routingbooleanOptionaltext content only: deliver via RCS where possible, SMS otherwise. Requires an approved RCS agent as the sender. Rich content always routes via RCS.
expiryintegerOptionalValidity in seconds. Min 60, max 259,200. Default 172,800 (48 h).
customer_idstringOptionalYour reference, max 128 chars. Returned in webhooks.
metadataobjectOptionalKey-value pairs; max 10 keys, values max 256 chars. Returned in webhooks.
sandboxbooleanOptionalForce test behaviour on a live key. Default false.
callback_urlstringOptionalPer-message webhook URL, max 2,048 chars.
Response · accepted
{
  "sandbox": false,
  "total": 1,
  "accepted": 1,
  "rejected": 0,
  "messages": [{
    "message_id": "msg_42a731415e521124",
    "to": "447700900123",
    "sender": "YOUR_SENDER_ID",
    "status": "queued",
    "channel": "SMS",
    "content_type": "text",
    "cost": 0.04
  }],
  "created_at": "2026-07-14T10:10:59+00:00"
}

cost is your account's contracted per-message rate in your account currency; the values shown in examples are illustrative, not a price list.

Error responses

401 UNAUTHORIZED · 422 VALIDATION_ERROR · 422 SENDER_NOT_RCS_AGENT · 422 INVALID_CONTENT · 422 CONTENT_TOO_LONG · 429 RATE_LIMITED, all in the error catalogue.

Events that may follow

message.status for every transition: submitted, delivered, read, failed, expired, rejected, fallback_delivered, fallback_failed.

GET /v2/messages/{message_id}

Retrieve a message

Current status and timestamps for a previously sent message. Webhooks are preferred; use this to reconcile.

Response · 200
{
  "message_id": "msg_42a731415e521124",
  "to": "447700900123",
  "sender": "MyBrandAgent",
  "channel": "RCS",
  "content_type": "rich_card",
  "created_at": "2026-07-14T10:10:59+01:00",
  "cost": 0.10,
  "status": "read",
  "read_at": "2026-07-14T10:11:12+01:00"
}

Failed messages include a failure object; fallen-back messages include fell_back_to_sms: true and message_parts.

Contact Book endpoints

The Contact Book API has its own complete reference covering all 47 endpoints, every scope, parameter and response: Contact Book reference.

Contact Book API v1.1 Updated 21 July 2026 · View changelog →

Contact Book reference

Every Contact Book API v1 endpoint: 47 operations across contacts, tags, lists, opt-outs and custom fields, plus operations and diagnostics. Base URL https://api.quicksms.com/v1, Bearer cbk_… token, and the shared error, idempotency and pagination conventions throughout. Expand any endpoint for parameters, examples and responses.

Contacts

Manage individual contacts and run bulk operations.

GET /v1/contacts contacts:read

List contacts

Paginated list of contacts with filtering, search and sorting. Returns up to 1000 per page.

Parameters, examples and response
NameInTypeRequiredDescription
search query string Optional Substring match on name, email, mobile.
status query enum Optional active or opted_out. Computed from opt-out records.
tag_id query uuid Optional Filter to contacts having this tag.
list_id query uuid Optional Filter to contacts in this list.
country query string Optional ISO-3166-1 alpha-2 country code.
date_from / date_to query date Optional Created-at bounds (YYYY-MM-DD).
per_page / page query integer Optional Default 25, max 1000; 1-indexed page.
sort_by / sort_dir query enum Optional first_name, last_name, mobile_number, status, created_at (default); asc or desc (default).
Example request
curl 'https://api.quicksms.com/v1/contacts?per_page=25&status=active' \
  -H 'Authorization: Bearer cbk_test_your_token'
Example request
const response = await fetch('https://api.quicksms.com/v1/contacts?per_page=25&status=active', {
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
  },
});
const data = await response.json();
Example request
import requests

response = requests.get(
    'https://api.quicksms.com/v1/contacts?per_page=25&status=active',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
)
data = response.json()
Response · 200
{
    "data": [
        {
            "id": "9c8b1a2e-…",
            "mobile_number": "+447700900123",
            "first_name": "Alice",
            "last_name": "Smith",
            "email": "alice@example.com",
            "status": "active",
            "source": "manual",
            "created_at": "2026-05-30T14:22:11Z"
        }
    ],
    "total": 152,
    "per_page": 25,
    "current_page": 1,
    "last_page": 7
}
GET /v1/contacts/{id} contacts:read

Get a contact

Returns a single contact with its tags, lists and custom data.

Parameters, examples and response
NameInTypeRequiredDescription
id path uuid Required Contact UUID.
Example request
curl 'https://api.quicksms.com/v1/contacts/uuid' \
  -H 'Authorization: Bearer cbk_test_your_token'
Example request
const response = await fetch('https://api.quicksms.com/v1/contacts/uuid', {
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
  },
});
const data = await response.json();
Example request
import requests

response = requests.get(
    'https://api.quicksms.com/v1/contacts/uuid',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
)
data = response.json()
Response · 200
{
    "data": {
        "id": "9c8b1a2e-…",
        "mobile_number": "+447700900123",
        "first_name": "Alice",
        "last_name": "Smith",
        "email": "alice@example.com",
        "date_of_birth": "1990-04-15",
        "postcode": "SW1A 1AA",
        "city": "London",
        "country": "GB",
        "customer_id": "shopify_cus_1234567",
        "status": "active",
        "tags": [
            {
                "id": "uuid",
                "name": "VIP",
                "color": "#6f42c1"
            }
        ],
        "lists": [
            {
                "id": "uuid",
                "name": "Newsletter Subscribers"
            }
        ],
        "custom_data": {
            "loyalty_tier": "gold"
        },
        "created_at": "2026-05-30T14:22:11Z"
    }
}
POST /v1/contacts contacts:write

Create a contact

Creates a new contact. Only mobile_number is required.

Parameters, examples and response
Example request
curl -X POST 'https://api.quicksms.com/v1/contacts' \
  -H 'Authorization: Bearer cbk_test_your_token' \
  -H 'Content-Type: application/json' \
  -d '{"mobile_number":"+447700900123","first_name":"Alice","last_name":"Smith","email":"alice@example.com","customer_id":"shopify_cus_1234567","country":"GB","custom_data":{"loyalty_tier":"gold"}}'
Example request
const response = await fetch('https://api.quicksms.com/v1/contacts', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "mobile_number": "+447700900123",
    "first_name": "Alice",
    "last_name": "Smith",
    "email": "alice@example.com",
    "customer_id": "shopify_cus_1234567",
    "country": "GB",
    "custom_data": {
        "loyalty_tier": "gold"
    }
}),
});
const data = await response.json();
Example request
import requests

response = requests.post(
    'https://api.quicksms.com/v1/contacts',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
    json={
    "mobile_number": "+447700900123",
    "first_name": "Alice",
    "last_name": "Smith",
    "email": "alice@example.com",
    "customer_id": "shopify_cus_1234567",
    "country": "GB",
    "custom_data": {
        "loyalty_tier": "gold"
    }
},
)
data = response.json()
Response · 201
{
    "data": {
        "id": "9c8b1a2e-…",
        "mobile_number": "+447700900123",
        "first_name": "Alice",
        "status": "active",
        "created_at": "2026-06-03T07:30:00Z"
    }
}
PUT /v1/contacts/{id} contacts:write

Update a contact

Updates a contact. Only supplied fields are changed.

Parameters, examples and response
Example request
curl -X PUT 'https://api.quicksms.com/v1/contacts/uuid' \
  -H 'Authorization: Bearer cbk_test_your_token' \
  -H 'Content-Type: application/json' \
  -d '{"first_name":"Alicia","email":"alicia@example.com"}'
Example request
const response = await fetch('https://api.quicksms.com/v1/contacts/uuid', {
  method: 'PUT',
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "first_name": "Alicia",
    "email": "alicia@example.com"
}),
});
const data = await response.json();
Example request
import requests

response = requests.put(
    'https://api.quicksms.com/v1/contacts/uuid',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
    json={
    "first_name": "Alicia",
    "email": "alicia@example.com"
},
)
data = response.json()
Response · 200
{
    "data": {
        "id": "9c8b1a2e-…",
        "first_name": "Alicia",
        "email": "alicia@example.com"
    }
}
DELETE /v1/contacts/{id} contacts:write

Delete a contact

Soft-deletes a contact. List memberships are preserved for restoration.

Parameters, examples and response
Example request
curl -X DELETE 'https://api.quicksms.com/v1/contacts/uuid' \
  -H 'Authorization: Bearer cbk_test_your_token'
Example request
const response = await fetch('https://api.quicksms.com/v1/contacts/uuid', {
  method: 'DELETE',
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
  },
});
const data = await response.json();
Example request
import requests

response = requests.delete(
    'https://api.quicksms.com/v1/contacts/uuid',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
)
data = response.json()
Response · 200
{
    "data": {
        "message": "Contact deleted"
    }
}
GET /v1/contacts/{id}/mobile contacts:read

Reveal full mobile number

Returns the unmasked mobile number. External API callers receive unmasked numbers on every contact endpoint, so this is primarily for portal-context audit trails.

Parameters, examples and response
Example request
curl 'https://api.quicksms.com/v1/contacts/uuid/mobile' \
  -H 'Authorization: Bearer cbk_test_your_token'
Example request
const response = await fetch('https://api.quicksms.com/v1/contacts/uuid/mobile', {
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
  },
});
const data = await response.json();
Example request
import requests

response = requests.get(
    'https://api.quicksms.com/v1/contacts/uuid/mobile',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
)
data = response.json()
Response · 200
{
    "mobile_number": "+447700900123"
}
GET /v1/contacts/stats contacts:read

Contact counts

Lightweight KPI counts. Accepts the same filter params as /contacts.

Parameters, examples and response
Example request
curl 'https://api.quicksms.com/v1/contacts/stats' \
  -H 'Authorization: Bearer cbk_test_your_token'
Example request
const response = await fetch('https://api.quicksms.com/v1/contacts/stats', {
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
  },
});
const data = await response.json();
Example request
import requests

response = requests.get(
    'https://api.quicksms.com/v1/contacts/stats',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
)
data = response.json()
Response · 200
{
    "total": 152,
    "active": 148,
    "opted_out": 4,
    "recent_7d": 12
}
POST /v1/contacts/bulk-import contacts:write

Bulk-import contacts

Upserts up to 5000 contacts per call. Existing contacts (matched by mobile_number) are left untouched. Idempotent. Rate-limited to 60 calls per minute.

Parameters, examples and response
Example request
curl -X POST 'https://api.quicksms.com/v1/contacts/bulk-import' \
  -H 'Authorization: Bearer cbk_test_your_token' \
  -H 'Content-Type: application/json' \
  -d '{"contacts":[{"mobile_number":"+447700900001","first_name":"Alice"},{"mobile_number":"+447700900002","first_name":"Bob"}]}'
Example request
const response = await fetch('https://api.quicksms.com/v1/contacts/bulk-import', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "contacts": [
        {
            "mobile_number": "+447700900001",
            "first_name": "Alice"
        },
        {
            "mobile_number": "+447700900002",
            "first_name": "Bob"
        }
    ]
}),
});
const data = await response.json();
Example request
import requests

response = requests.post(
    'https://api.quicksms.com/v1/contacts/bulk-import',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
    json={
    "contacts": [
        {
            "mobile_number": "+447700900001",
            "first_name": "Alice"
        },
        {
            "mobile_number": "+447700900002",
            "first_name": "Bob"
        }
    ]
},
)
data = response.json()
Response · 200
{
    "data": {
        "inserted": 2,
        "skipped": 0,
        "total": 2
    }
}
POST /v1/contacts/bulk/add-tags contacts:write

Add tags to many contacts

Adds tags to up to 1000 contacts. Missing tags are created. Idempotent.

Parameters, examples and response
Example request
curl -X POST 'https://api.quicksms.com/v1/contacts/bulk/add-tags' \
  -H 'Authorization: Bearer cbk_test_your_token' \
  -H 'Content-Type: application/json' \
  -d '{"contact_ids":["uuid-1","uuid-2"],"tags":["VIP","Newsletter"]}'
Example request
const response = await fetch('https://api.quicksms.com/v1/contacts/bulk/add-tags', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "contact_ids": [
        "uuid-1",
        "uuid-2"
    ],
    "tags": [
        "VIP",
        "Newsletter"
    ]
}),
});
const data = await response.json();
Example request
import requests

response = requests.post(
    'https://api.quicksms.com/v1/contacts/bulk/add-tags',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
    json={
    "contact_ids": [
        "uuid-1",
        "uuid-2"
    ],
    "tags": [
        "VIP",
        "Newsletter"
    ]
},
)
data = response.json()
Response · 200
{
    "data": {
        "message": "Added tag(s) \"VIP\" to 2 contact(s)",
        "affectedCount": 2
    }
}
POST /v1/contacts/bulk/remove-tags contacts:write

Remove tags from many contacts

Removes tags from up to 1000 contacts. Tags that don't resolve are silently ignored.

Parameters, examples and response
Example request
curl -X POST 'https://api.quicksms.com/v1/contacts/bulk/remove-tags' \
  -H 'Authorization: Bearer cbk_test_your_token' \
  -H 'Content-Type: application/json' \
  -d '{"contact_ids":["uuid-1"],"tags":["VIP"]}'
Example request
const response = await fetch('https://api.quicksms.com/v1/contacts/bulk/remove-tags', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "contact_ids": [
        "uuid-1"
    ],
    "tags": [
        "VIP"
    ]
}),
});
const data = await response.json();
Example request
import requests

response = requests.post(
    'https://api.quicksms.com/v1/contacts/bulk/remove-tags',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
    json={
    "contact_ids": [
        "uuid-1"
    ],
    "tags": [
        "VIP"
    ]
},
)
data = response.json()
Response · 200
{
    "data": {
        "affectedCount": 1
    }
}
POST /v1/contacts/bulk/add-to-list contacts:write

Add contacts to a list

Adds up to 1000 contacts to a list, addressed by list name. Idempotent.

Parameters, examples and response
Example request
curl -X POST 'https://api.quicksms.com/v1/contacts/bulk/add-to-list' \
  -H 'Authorization: Bearer cbk_test_your_token' \
  -H 'Content-Type: application/json' \
  -d '{"contact_ids":["uuid-1"],"list_name":"Newsletter Subscribers"}'
Example request
const response = await fetch('https://api.quicksms.com/v1/contacts/bulk/add-to-list', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "contact_ids": [
        "uuid-1"
    ],
    "list_name": "Newsletter Subscribers"
}),
});
const data = await response.json();
Example request
import requests

response = requests.post(
    'https://api.quicksms.com/v1/contacts/bulk/add-to-list',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
    json={
    "contact_ids": [
        "uuid-1"
    ],
    "list_name": "Newsletter Subscribers"
},
)
data = response.json()
Response · 200
{
    "data": {
        "affectedCount": 1
    }
}
POST /v1/contacts/bulk/remove-from-list contacts:write

Remove contacts from a list

Removes up to 1000 contacts from a list, addressed by list name.

Parameters, examples and response
Example request
curl -X POST 'https://api.quicksms.com/v1/contacts/bulk/remove-from-list' \
  -H 'Authorization: Bearer cbk_test_your_token' \
  -H 'Content-Type: application/json' \
  -d '{"contact_ids":["uuid-1"],"list_name":"Newsletter Subscribers"}'
Example request
const response = await fetch('https://api.quicksms.com/v1/contacts/bulk/remove-from-list', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "contact_ids": [
        "uuid-1"
    ],
    "list_name": "Newsletter Subscribers"
}),
});
const data = await response.json();
Example request
import requests

response = requests.post(
    'https://api.quicksms.com/v1/contacts/bulk/remove-from-list',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
    json={
    "contact_ids": [
        "uuid-1"
    ],
    "list_name": "Newsletter Subscribers"
},
)
data = response.json()
Response · 200
{
    "data": {
        "affectedCount": 1
    }
}
POST /v1/contacts/bulk/add-to-opt-out contacts:write

Opt out many contacts

Adds the mobile numbers of up to 1000 contacts to an opt-out list. Idempotent.

Parameters, examples and response
Example request
curl -X POST 'https://api.quicksms.com/v1/contacts/bulk/add-to-opt-out' \
  -H 'Authorization: Bearer cbk_test_your_token' \
  -H 'Content-Type: application/json' \
  -d '{"contact_ids":["uuid-1"],"opt_out_list_id":"list-uuid"}'
Example request
const response = await fetch('https://api.quicksms.com/v1/contacts/bulk/add-to-opt-out', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "contact_ids": [
        "uuid-1"
    ],
    "opt_out_list_id": "list-uuid"
}),
});
const data = await response.json();
Example request
import requests

response = requests.post(
    'https://api.quicksms.com/v1/contacts/bulk/add-to-opt-out',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
    json={
    "contact_ids": [
        "uuid-1"
    ],
    "opt_out_list_id": "list-uuid"
},
)
data = response.json()
Response · 200
{
    "data": {
        "affectedCount": 1
    }
}
POST /v1/contacts/bulk/remove-from-opt-out contacts:write

Remove from opt-out

Removes the mobile numbers of up to 1000 contacts from a specific opt-out list.

Parameters, examples and response
Example request
curl -X POST 'https://api.quicksms.com/v1/contacts/bulk/remove-from-opt-out' \
  -H 'Authorization: Bearer cbk_test_your_token' \
  -H 'Content-Type: application/json' \
  -d '{"contact_ids":["uuid-1"],"opt_out_list_id":"list-uuid"}'
Example request
const response = await fetch('https://api.quicksms.com/v1/contacts/bulk/remove-from-opt-out', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "contact_ids": [
        "uuid-1"
    ],
    "opt_out_list_id": "list-uuid"
}),
});
const data = await response.json();
Example request
import requests

response = requests.post(
    'https://api.quicksms.com/v1/contacts/bulk/remove-from-opt-out',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
    json={
    "contact_ids": [
        "uuid-1"
    ],
    "opt_out_list_id": "list-uuid"
},
)
data = response.json()
Response · 200
{
    "data": {
        "affectedCount": 1
    }
}
POST /v1/contacts/bulk/delete contacts:write

Bulk soft-delete contacts

Soft-deletes up to 1000 contacts. List memberships preserved for restoration.

Parameters, examples and response
Example request
curl -X POST 'https://api.quicksms.com/v1/contacts/bulk/delete' \
  -H 'Authorization: Bearer cbk_test_your_token' \
  -H 'Content-Type: application/json' \
  -d '{"contact_ids":["uuid-1","uuid-2"]}'
Example request
const response = await fetch('https://api.quicksms.com/v1/contacts/bulk/delete', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "contact_ids": [
        "uuid-1",
        "uuid-2"
    ]
}),
});
const data = await response.json();
Example request
import requests

response = requests.post(
    'https://api.quicksms.com/v1/contacts/bulk/delete',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
    json={
    "contact_ids": [
        "uuid-1",
        "uuid-2"
    ]
},
)
data = response.json()
Response · 200
{
    "data": {
        "affectedCount": 2
    }
}
POST /v1/contacts/bulk/add-tags-by-filter contacts:write

Add tags by filter (async)

Asynchronous variant of /contacts/bulk/add-tags for operations over 1000 contacts. Returns immediately with an operation_id; poll GET /operations/{id}.

Parameters, examples and response
Example request
curl -X POST 'https://api.quicksms.com/v1/contacts/bulk/add-tags-by-filter' \
  -H 'Authorization: Bearer cbk_test_your_token' \
  -H 'Content-Type: application/json' \
  -d '{"filter":{"status":"active","country":"GB"},"expected_count":50000,"tags":["UK Active"]}'
Example request
const response = await fetch('https://api.quicksms.com/v1/contacts/bulk/add-tags-by-filter', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "filter": {
        "status": "active",
        "country": "GB"
    },
    "expected_count": 50000,
    "tags": [
        "UK Active"
    ]
}),
});
const data = await response.json();
Example request
import requests

response = requests.post(
    'https://api.quicksms.com/v1/contacts/bulk/add-tags-by-filter',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
    json={
    "filter": {
        "status": "active",
        "country": "GB"
    },
    "expected_count": 50000,
    "tags": [
        "UK Active"
    ]
},
)
data = response.json()
Response · 202
{
    "data": {
        "operation_id": "op-uuid",
        "status": "queued",
        "total_items": 50000
    }
}
POST /v1/contacts/bulk/remove-tags-by-filter contacts:write

Remove tags by filter (async)

Asynchronous variant of /contacts/bulk/remove-tags. Tags that don't resolve are skipped.

Parameters, examples and response
Example request
curl -X POST 'https://api.quicksms.com/v1/contacts/bulk/remove-tags-by-filter' \
  -H 'Authorization: Bearer cbk_test_your_token' \
  -H 'Content-Type: application/json' \
  -d '{"filter":{"tag_id":"old-tag-uuid"},"expected_count":12000,"tags":["Old Promo"]}'
Example request
const response = await fetch('https://api.quicksms.com/v1/contacts/bulk/remove-tags-by-filter', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "filter": {
        "tag_id": "old-tag-uuid"
    },
    "expected_count": 12000,
    "tags": [
        "Old Promo"
    ]
}),
});
const data = await response.json();
Example request
import requests

response = requests.post(
    'https://api.quicksms.com/v1/contacts/bulk/remove-tags-by-filter',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
    json={
    "filter": {
        "tag_id": "old-tag-uuid"
    },
    "expected_count": 12000,
    "tags": [
        "Old Promo"
    ]
},
)
data = response.json()
Response · 202
{
    "data": {
        "operation_id": "op-uuid",
        "status": "queued",
        "total_items": 12000
    }
}
POST /v1/contacts/bulk/add-to-list-by-filter contacts:write

Add to list by filter (async)

Asynchronous variant of /contacts/bulk/add-to-list.

Parameters, examples and response
Example request
curl -X POST 'https://api.quicksms.com/v1/contacts/bulk/add-to-list-by-filter' \
  -H 'Authorization: Bearer cbk_test_your_token' \
  -H 'Content-Type: application/json' \
  -d '{"filter":{"country":"GB"},"expected_count":30000,"list_name":"UK Customers"}'
Example request
const response = await fetch('https://api.quicksms.com/v1/contacts/bulk/add-to-list-by-filter', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "filter": {
        "country": "GB"
    },
    "expected_count": 30000,
    "list_name": "UK Customers"
}),
});
const data = await response.json();
Example request
import requests

response = requests.post(
    'https://api.quicksms.com/v1/contacts/bulk/add-to-list-by-filter',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
    json={
    "filter": {
        "country": "GB"
    },
    "expected_count": 30000,
    "list_name": "UK Customers"
},
)
data = response.json()
Response · 202
{
    "data": {
        "operation_id": "op-uuid",
        "status": "queued",
        "total_items": 30000
    }
}
POST /v1/contacts/bulk/remove-from-list-by-filter contacts:write

Remove from list by filter (async)

Asynchronous variant of /contacts/bulk/remove-from-list.

Parameters, examples and response
Example request
curl -X POST 'https://api.quicksms.com/v1/contacts/bulk/remove-from-list-by-filter' \
  -H 'Authorization: Bearer cbk_test_your_token' \
  -H 'Content-Type: application/json' \
  -d '{"filter":{"list_id":"list-uuid"},"expected_count":5000,"list_name":"Old List"}'
Example request
const response = await fetch('https://api.quicksms.com/v1/contacts/bulk/remove-from-list-by-filter', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "filter": {
        "list_id": "list-uuid"
    },
    "expected_count": 5000,
    "list_name": "Old List"
}),
});
const data = await response.json();
Example request
import requests

response = requests.post(
    'https://api.quicksms.com/v1/contacts/bulk/remove-from-list-by-filter',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
    json={
    "filter": {
        "list_id": "list-uuid"
    },
    "expected_count": 5000,
    "list_name": "Old List"
},
)
data = response.json()
Response · 202
{
    "data": {
        "operation_id": "op-uuid",
        "status": "queued",
        "total_items": 5000
    }
}
POST /v1/contacts/bulk/delete-by-filter contacts:write

Soft-delete by filter (async)

Asynchronous variant of /contacts/bulk/delete, with the same expected_count drift-tolerance check.

Parameters, examples and response
Example request
curl -X POST 'https://api.quicksms.com/v1/contacts/bulk/delete-by-filter' \
  -H 'Authorization: Bearer cbk_test_your_token' \
  -H 'Content-Type: application/json' \
  -d '{"filter":{"status":"opted_out"},"expected_count":8000}'
Example request
const response = await fetch('https://api.quicksms.com/v1/contacts/bulk/delete-by-filter', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "filter": {
        "status": "opted_out"
    },
    "expected_count": 8000
}),
});
const data = await response.json();
Example request
import requests

response = requests.post(
    'https://api.quicksms.com/v1/contacts/bulk/delete-by-filter',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
    json={
    "filter": {
        "status": "opted_out"
    },
    "expected_count": 8000
},
)
data = response.json()
Response · 202
{
    "data": {
        "operation_id": "op-uuid",
        "status": "queued",
        "total_items": 8000
    }
}

Tags

Manage labels for grouping contacts. Tags are account-scoped and may have a colour.

GET /v1/tags tags:read

List tags

Returns all tags for the account, alphabetised.

Parameters, examples and response
NameInTypeRequiredDescription
search query string Optional Substring match on tag name.
Example request
curl 'https://api.quicksms.com/v1/tags' \
  -H 'Authorization: Bearer cbk_test_your_token'
Example request
const response = await fetch('https://api.quicksms.com/v1/tags', {
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
  },
});
const data = await response.json();
Example request
import requests

response = requests.get(
    'https://api.quicksms.com/v1/tags',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
)
data = response.json()
Response · 200
{
    "data": [
        {
            "id": "uuid",
            "name": "VIP",
            "color": "#6f42c1",
            "contact_count": 42
        },
        {
            "id": "uuid",
            "name": "Newsletter",
            "color": "#0d6efd",
            "contact_count": 1024
        }
    ]
}
POST /v1/tags tags:write

Create a tag

Creates a new tag. Names must be unique within the account.

Parameters, examples and response
Example request
curl -X POST 'https://api.quicksms.com/v1/tags' \
  -H 'Authorization: Bearer cbk_test_your_token' \
  -H 'Content-Type: application/json' \
  -d '{"name":"VIP","color":"#6f42c1"}'
Example request
const response = await fetch('https://api.quicksms.com/v1/tags', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "name": "VIP",
    "color": "#6f42c1"
}),
});
const data = await response.json();
Example request
import requests

response = requests.post(
    'https://api.quicksms.com/v1/tags',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
    json={
    "name": "VIP",
    "color": "#6f42c1"
},
)
data = response.json()
Response · 201
{
    "data": {
        "id": "uuid",
        "name": "VIP",
        "color": "#6f42c1",
        "contact_count": 0
    }
}
PUT /v1/tags/{id} tags:write

Update a tag

Updates a tag's name and/or colour.

Parameters, examples and response
Example request
curl -X PUT 'https://api.quicksms.com/v1/tags/uuid' \
  -H 'Authorization: Bearer cbk_test_your_token' \
  -H 'Content-Type: application/json' \
  -d '{"name":"VVIP","color":"#dc2626"}'
Example request
const response = await fetch('https://api.quicksms.com/v1/tags/uuid', {
  method: 'PUT',
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "name": "VVIP",
    "color": "#dc2626"
}),
});
const data = await response.json();
Example request
import requests

response = requests.put(
    'https://api.quicksms.com/v1/tags/uuid',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
    json={
    "name": "VVIP",
    "color": "#dc2626"
},
)
data = response.json()
Response · 200
{
    "data": {
        "id": "uuid",
        "name": "VVIP",
        "color": "#dc2626"
    }
}
DELETE /v1/tags/{id} tags:write

Delete a tag

Deletes a tag and removes all contact-tag associations.

Parameters, examples and response
Example request
curl -X DELETE 'https://api.quicksms.com/v1/tags/uuid' \
  -H 'Authorization: Bearer cbk_test_your_token'
Example request
const response = await fetch('https://api.quicksms.com/v1/tags/uuid', {
  method: 'DELETE',
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
  },
});
const data = await response.json();
Example request
import requests

response = requests.delete(
    'https://api.quicksms.com/v1/tags/uuid',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
)
data = response.json()
Response · 200
{
    "data": {
        "message": "Tag deleted"
    }
}

Lists

Manage contact lists (static membership) and their members.

GET /v1/lists lists:read

List all lists

Returns all lists for the account.

Parameters, examples and response
NameInTypeRequiredDescription
search query string Optional Substring match on list name.
type query enum Optional static or dynamic.
Example request
curl 'https://api.quicksms.com/v1/lists' \
  -H 'Authorization: Bearer cbk_test_your_token'
Example request
const response = await fetch('https://api.quicksms.com/v1/lists', {
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
  },
});
const data = await response.json();
Example request
import requests

response = requests.get(
    'https://api.quicksms.com/v1/lists',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
)
data = response.json()
Response · 200
{
    "data": [
        {
            "id": "uuid",
            "name": "Newsletter Subscribers",
            "description": "Opted-in subscribers",
            "type": "static",
            "contact_count": 1024,
            "created_at": "2026-05-01T10:00:00Z"
        }
    ]
}
POST /v1/lists lists:write

Create a list

Creates a new list, optionally seeded with initial contact_ids.

Parameters, examples and response
Example request
curl -X POST 'https://api.quicksms.com/v1/lists' \
  -H 'Authorization: Bearer cbk_test_your_token' \
  -H 'Content-Type: application/json' \
  -d '{"name":"VIP Customers","description":"High-value customer list","contact_ids":["uuid-1","uuid-2"]}'
Example request
const response = await fetch('https://api.quicksms.com/v1/lists', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "name": "VIP Customers",
    "description": "High-value customer list",
    "contact_ids": [
        "uuid-1",
        "uuid-2"
    ]
}),
});
const data = await response.json();
Example request
import requests

response = requests.post(
    'https://api.quicksms.com/v1/lists',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
    json={
    "name": "VIP Customers",
    "description": "High-value customer list",
    "contact_ids": [
        "uuid-1",
        "uuid-2"
    ]
},
)
data = response.json()
Response · 201
{
    "data": {
        "id": "uuid",
        "name": "VIP Customers",
        "contact_count": 2,
        "type": "static"
    }
}
PUT /v1/lists/{id} lists:write

Update a list

Updates a list's metadata. Contents are managed through the members endpoints.

Parameters, examples and response
Example request
curl -X PUT 'https://api.quicksms.com/v1/lists/uuid' \
  -H 'Authorization: Bearer cbk_test_your_token' \
  -H 'Content-Type: application/json' \
  -d '{"name":"VIP Customers (2026)","description":"Updated description"}'
Example request
const response = await fetch('https://api.quicksms.com/v1/lists/uuid', {
  method: 'PUT',
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "name": "VIP Customers (2026)",
    "description": "Updated description"
}),
});
const data = await response.json();
Example request
import requests

response = requests.put(
    'https://api.quicksms.com/v1/lists/uuid',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
    json={
    "name": "VIP Customers (2026)",
    "description": "Updated description"
},
)
data = response.json()
Response · 200
{
    "data": {
        "id": "uuid",
        "name": "VIP Customers (2026)"
    }
}
DELETE /v1/lists/{id} lists:write

Delete a list

Deletes a list and removes all contact-list associations. The contacts themselves are not affected.

Parameters, examples and response
Example request
curl -X DELETE 'https://api.quicksms.com/v1/lists/uuid' \
  -H 'Authorization: Bearer cbk_test_your_token'
Example request
const response = await fetch('https://api.quicksms.com/v1/lists/uuid', {
  method: 'DELETE',
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
  },
});
const data = await response.json();
Example request
import requests

response = requests.delete(
    'https://api.quicksms.com/v1/lists/uuid',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
)
data = response.json()
Response · 200
{
    "data": {
        "message": "List deleted"
    }
}
POST /v1/lists/{id}/members lists:write

Add members to a list

Adds contacts to a list. Idempotent.

Parameters, examples and response
Example request
curl -X POST 'https://api.quicksms.com/v1/lists/uuid/members' \
  -H 'Authorization: Bearer cbk_test_your_token' \
  -H 'Content-Type: application/json' \
  -d '{"contact_ids":["uuid-1","uuid-2"]}'
Example request
const response = await fetch('https://api.quicksms.com/v1/lists/uuid/members', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "contact_ids": [
        "uuid-1",
        "uuid-2"
    ]
}),
});
const data = await response.json();
Example request
import requests

response = requests.post(
    'https://api.quicksms.com/v1/lists/uuid/members',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
    json={
    "contact_ids": [
        "uuid-1",
        "uuid-2"
    ]
},
)
data = response.json()
Response · 200
{
    "data": {
        "added": 2
    }
}
DELETE /v1/lists/{id}/members lists:write

Remove members from a list

Removes contacts from a list. Returns the actual delete count.

Parameters, examples and response
Example request
curl -X DELETE 'https://api.quicksms.com/v1/lists/uuid/members' \
  -H 'Authorization: Bearer cbk_test_your_token' \
  -H 'Content-Type: application/json' \
  -d '{"contact_ids":["uuid-1"]}'
Example request
const response = await fetch('https://api.quicksms.com/v1/lists/uuid/members', {
  method: 'DELETE',
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "contact_ids": [
        "uuid-1"
    ]
}),
});
const data = await response.json();
Example request
import requests

response = requests.delete(
    'https://api.quicksms.com/v1/lists/uuid/members',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
    json={
    "contact_ids": [
        "uuid-1"
    ]
},
)
data = response.json()
Response · 200
{
    "data": {
        "removed": 1
    }
}
POST /v1/lists/{id}/members/bulk-import lists:write

Bulk-import contacts into a list

Upserts up to 5000 contacts per call and adds them to the list. Existing contacts (matched by mobile_number) are left untouched but still added. Idempotent on re-import. Rate-limited to 60 calls per minute.

Parameters, examples and response
Example request
curl -X POST 'https://api.quicksms.com/v1/lists/uuid/members/bulk-import' \
  -H 'Authorization: Bearer cbk_test_your_token' \
  -H 'Content-Type: application/json' \
  -d '{"contacts":[{"mobile_number":"+447700900001","first_name":"Alice"},{"mobile_number":"+447700900002"}]}'
Example request
const response = await fetch('https://api.quicksms.com/v1/lists/uuid/members/bulk-import', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "contacts": [
        {
            "mobile_number": "+447700900001",
            "first_name": "Alice"
        },
        {
            "mobile_number": "+447700900002"
        }
    ]
}),
});
const data = await response.json();
Example request
import requests

response = requests.post(
    'https://api.quicksms.com/v1/lists/uuid/members/bulk-import',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
    json={
    "contacts": [
        {
            "mobile_number": "+447700900001",
            "first_name": "Alice"
        },
        {
            "mobile_number": "+447700900002"
        }
    ]
},
)
data = response.json()
Response · 200
{
    "data": {
        "new_contacts": 1,
        "existing_contacts": 1,
        "added_to_list": 2
    }
}

Opt-outs

Manage opt-out lists and individual records. Use the check endpoint before sending to verify consent.

GET /v1/opt-out-lists opt-outs:read

List opt-out lists

Returns all opt-out lists for the account. The master opt-out list always appears first.

Parameters, examples and response
NameInTypeRequiredDescription
search query string Optional Substring match on list name.
Example request
curl 'https://api.quicksms.com/v1/opt-out-lists' \
  -H 'Authorization: Bearer cbk_test_your_token'
Example request
const response = await fetch('https://api.quicksms.com/v1/opt-out-lists', {
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
  },
});
const data = await response.json();
Example request
import requests

response = requests.get(
    'https://api.quicksms.com/v1/opt-out-lists',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
)
data = response.json()
Response · 200
{
    "data": [
        {
            "id": "uuid",
            "name": "Master Opt-Out",
            "is_master": true,
            "count": 4
        },
        {
            "id": "uuid",
            "name": "Campaign X Opt-Outs",
            "is_master": false,
            "count": 12
        }
    ]
}
POST /v1/opt-out-lists opt-outs:write

Create an opt-out list

Creates an opt-out list. The is_master flag is system-managed and cannot be set by API.

Parameters, examples and response
Example request
curl -X POST 'https://api.quicksms.com/v1/opt-out-lists' \
  -H 'Authorization: Bearer cbk_test_your_token' \
  -H 'Content-Type: application/json' \
  -d '{"name":"Campaign X Opt-Outs","description":"Opt-outs from the Campaign X promo"}'
Example request
const response = await fetch('https://api.quicksms.com/v1/opt-out-lists', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "name": "Campaign X Opt-Outs",
    "description": "Opt-outs from the Campaign X promo"
}),
});
const data = await response.json();
Example request
import requests

response = requests.post(
    'https://api.quicksms.com/v1/opt-out-lists',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
    json={
    "name": "Campaign X Opt-Outs",
    "description": "Opt-outs from the Campaign X promo"
},
)
data = response.json()
Response · 201
{
    "data": {
        "id": "uuid",
        "name": "Campaign X Opt-Outs",
        "is_master": false,
        "count": 0
    }
}
PUT /v1/opt-out-lists/{id} opt-outs:write

Update an opt-out list

Updates name and/or description.

Parameters, examples and response
Example request
curl -X PUT 'https://api.quicksms.com/v1/opt-out-lists/uuid' \
  -H 'Authorization: Bearer cbk_test_your_token' \
  -H 'Content-Type: application/json' \
  -d '{"name":"Campaign X Opt-Outs (closed)"}'
Example request
const response = await fetch('https://api.quicksms.com/v1/opt-out-lists/uuid', {
  method: 'PUT',
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "name": "Campaign X Opt-Outs (closed)"
}),
});
const data = await response.json();
Example request
import requests

response = requests.put(
    'https://api.quicksms.com/v1/opt-out-lists/uuid',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
    json={
    "name": "Campaign X Opt-Outs (closed)"
},
)
data = response.json()
Response · 200
{
    "data": {
        "id": "uuid",
        "name": "Campaign X Opt-Outs (closed)"
    }
}
DELETE /v1/opt-out-lists/{id} opt-outs:write

Delete an opt-out list

Deletes a non-master opt-out list and all its records. Returns 422 if you try to delete the master list.

Parameters, examples and response
Example request
curl -X DELETE 'https://api.quicksms.com/v1/opt-out-lists/uuid' \
  -H 'Authorization: Bearer cbk_test_your_token'
Example request
const response = await fetch('https://api.quicksms.com/v1/opt-out-lists/uuid', {
  method: 'DELETE',
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
  },
});
const data = await response.json();
Example request
import requests

response = requests.delete(
    'https://api.quicksms.com/v1/opt-out-lists/uuid',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
)
data = response.json()
Response · 200
{
    "data": {
        "message": "Opt-out list deleted"
    }
}
GET /v1/opt-out-lists/{listId}/records opt-outs:read

List records in an opt-out list

Paginated list of opt-out records within a specific opt-out list.

Parameters, examples and response
NameInTypeRequiredDescription
listId path uuid Required Opt-out list UUID.
search query string Optional Substring match on mobile number.
source query enum Optional manual, sms_reply, upload, and similar.
per_page query integer Optional Default 25, max 100.
Example request
curl 'https://api.quicksms.com/v1/opt-out-lists/list-uuid/records' \
  -H 'Authorization: Bearer cbk_test_your_token'
Example request
const response = await fetch('https://api.quicksms.com/v1/opt-out-lists/list-uuid/records', {
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
  },
});
const data = await response.json();
Example request
import requests

response = requests.get(
    'https://api.quicksms.com/v1/opt-out-lists/list-uuid/records',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
)
data = response.json()
Response · 200
{
    "data": [
        {
            "id": "uuid",
            "mobile_number": "+447700900099",
            "source": "sms_reply",
            "created_at": "2026-05-30T12:00:00Z"
        }
    ],
    "total": 1,
    "per_page": 25,
    "current_page": 1,
    "last_page": 1
}
GET /v1/opt-out-records opt-outs:read

List all opt-out records

Account-wide opt-out records list, optionally filtered by list with the opt_out_list_id query parameter.

Parameters, examples and response
Example request
curl 'https://api.quicksms.com/v1/opt-out-records?opt_out_list_id=list-uuid' \
  -H 'Authorization: Bearer cbk_test_your_token'
Example request
const response = await fetch('https://api.quicksms.com/v1/opt-out-records?opt_out_list_id=list-uuid', {
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
  },
});
const data = await response.json();
Example request
import requests

response = requests.get(
    'https://api.quicksms.com/v1/opt-out-records?opt_out_list_id=list-uuid',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
)
data = response.json()
Response · 200
{
    "data": [
        {
            "id": "uuid",
            "mobile_number": "+447700900099",
            "source": "sms_reply",
            "opt_out_list_id": "uuid",
            "created_at": "2026-05-30T12:00:00Z"
        }
    ],
    "total": 42,
    "per_page": 25,
    "current_page": 1,
    "last_page": 2
}
POST /v1/opt-out-lists/{listId}/records opt-outs:write

Add a number to an opt-out list

Adds a single mobile number to an opt-out list. Idempotent.

Parameters, examples and response
Example request
curl -X POST 'https://api.quicksms.com/v1/opt-out-lists/list-uuid/records' \
  -H 'Authorization: Bearer cbk_test_your_token' \
  -H 'Content-Type: application/json' \
  -d '{"mobile_number":"+447700900099","source":"manual","campaign_ref":"promo-2026"}'
Example request
const response = await fetch('https://api.quicksms.com/v1/opt-out-lists/list-uuid/records', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "mobile_number": "+447700900099",
    "source": "manual",
    "campaign_ref": "promo-2026"
}),
});
const data = await response.json();
Example request
import requests

response = requests.post(
    'https://api.quicksms.com/v1/opt-out-lists/list-uuid/records',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
    json={
    "mobile_number": "+447700900099",
    "source": "manual",
    "campaign_ref": "promo-2026"
},
)
data = response.json()
Response · 201
{
    "data": {
        "id": "uuid",
        "mobile_number": "+447700900099",
        "source": "manual"
    }
}
POST /v1/opt-out-lists/{listId}/records/bulk-import opt-outs:write

Bulk-import opt-out records

Adds up to 5000 mobile numbers per call to an opt-out list. Idempotent. Rate-limited to 60 calls per minute.

Parameters, examples and response
Example request
curl -X POST 'https://api.quicksms.com/v1/opt-out-lists/list-uuid/records/bulk-import' \
  -H 'Authorization: Bearer cbk_test_your_token' \
  -H 'Content-Type: application/json' \
  -d '{"records":[{"mobile_number":"+447700900099"},{"mobile_number":"+447700900100","source":"upload"}]}'
Example request
const response = await fetch('https://api.quicksms.com/v1/opt-out-lists/list-uuid/records/bulk-import', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "records": [
        {
            "mobile_number": "+447700900099"
        },
        {
            "mobile_number": "+447700900100",
            "source": "upload"
        }
    ]
}),
});
const data = await response.json();
Example request
import requests

response = requests.post(
    'https://api.quicksms.com/v1/opt-out-lists/list-uuid/records/bulk-import',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
    json={
    "records": [
        {
            "mobile_number": "+447700900099"
        },
        {
            "mobile_number": "+447700900100",
            "source": "upload"
        }
    ]
},
)
data = response.json()
Response · 200
{
    "data": {
        "inserted": 2,
        "skipped": 0,
        "total": 2
    }
}
DELETE /v1/opt-out-records/{id} opt-outs:write

Delete an opt-out record

Removes a single opt-out record by ID. Re-opts in the number if it is not present in other opt-out lists.

Parameters, examples and response
Example request
curl -X DELETE 'https://api.quicksms.com/v1/opt-out-records/uuid' \
  -H 'Authorization: Bearer cbk_test_your_token'
Example request
const response = await fetch('https://api.quicksms.com/v1/opt-out-records/uuid', {
  method: 'DELETE',
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
  },
});
const data = await response.json();
Example request
import requests

response = requests.delete(
    'https://api.quicksms.com/v1/opt-out-records/uuid',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
)
data = response.json()
Response · 200
{
    "data": {
        "message": "Opt-out record deleted"
    }
}
GET /v1/opt-out-records/{id}/mobile opt-outs:read

Reveal opt-out mobile

Returns the unmasked mobile number for an opt-out record.

Parameters, examples and response
Example request
curl 'https://api.quicksms.com/v1/opt-out-records/uuid/mobile' \
  -H 'Authorization: Bearer cbk_test_your_token'
Example request
const response = await fetch('https://api.quicksms.com/v1/opt-out-records/uuid/mobile', {
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
  },
});
const data = await response.json();
Example request
import requests

response = requests.get(
    'https://api.quicksms.com/v1/opt-out-records/uuid/mobile',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
)
data = response.json()
Response · 200
{
    "mobile_number": "+447700900099"
}
POST /v1/opt-out-records/move opt-outs:write

Move records between opt-out lists

Reassigns one or more opt-out records from one list to another.

Parameters, examples and response
Example request
curl -X POST 'https://api.quicksms.com/v1/opt-out-records/move' \
  -H 'Authorization: Bearer cbk_test_your_token' \
  -H 'Content-Type: application/json' \
  -d '{"record_ids":["uuid-1","uuid-2"],"target_opt_out_list_id":"list-uuid"}'
Example request
const response = await fetch('https://api.quicksms.com/v1/opt-out-records/move', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "record_ids": [
        "uuid-1",
        "uuid-2"
    ],
    "target_opt_out_list_id": "list-uuid"
}),
});
const data = await response.json();
Example request
import requests

response = requests.post(
    'https://api.quicksms.com/v1/opt-out-records/move',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
    json={
    "record_ids": [
        "uuid-1",
        "uuid-2"
    ],
    "target_opt_out_list_id": "list-uuid"
},
)
data = response.json()
Response · 200
{
    "data": {
        "moved": 2
    }
}
GET /v1/opt-outs/check opt-outs:read

Check opt-out status for a number

Queries all opt-out lists at once. Returns whether the mobile is opted out anywhere, plus which lists. Call this before sending to verify consent.

Parameters, examples and response
NameInTypeRequiredDescription
mobile_number query string Required Mobile number in E.164 format (e.g. +447700900099). URL-encode the leading + as %2B.
Example request
curl 'https://api.quicksms.com/v1/opt-outs/check?mobile_number=%2B447700900099' \
  -H 'Authorization: Bearer cbk_test_your_token'
Example request
const response = await fetch('https://api.quicksms.com/v1/opt-outs/check?mobile_number=%2B447700900099', {
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
  },
});
const data = await response.json();
Example request
import requests

response = requests.get(
    'https://api.quicksms.com/v1/opt-outs/check?mobile_number=%2B447700900099',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
)
data = response.json()
Response · 200
{
    "mobile_number": "+447700900099",
    "is_opted_out": true,
    "lists": [
        {
            "id": "uuid",
            "name": "Master Opt-Out",
            "is_master": true,
            "source": "sms_reply"
        }
    ]
}

Custom fields

Manage custom field definitions for contacts. Values are stored in each contact's custom_data object.

GET /v1/custom-fields custom-fields:read

List custom field definitions

Returns all custom field definitions for the account.

Parameters, examples and response
Example request
curl 'https://api.quicksms.com/v1/custom-fields' \
  -H 'Authorization: Bearer cbk_test_your_token'
Example request
const response = await fetch('https://api.quicksms.com/v1/custom-fields', {
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
  },
});
const data = await response.json();
Example request
import requests

response = requests.get(
    'https://api.quicksms.com/v1/custom-fields',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
)
data = response.json()
Response · 200
{
    "data": [
        {
            "id": "uuid",
            "key": "loyalty_tier",
            "label": "Loyalty Tier",
            "type": "string"
        },
        {
            "id": "uuid",
            "key": "join_date",
            "label": "Join Date",
            "type": "date"
        }
    ]
}
POST /v1/custom-fields custom-fields:write

Create a custom field

The key is used in contact custom_data; the label is the display name.

Parameters, examples and response
Example request
curl -X POST 'https://api.quicksms.com/v1/custom-fields' \
  -H 'Authorization: Bearer cbk_test_your_token' \
  -H 'Content-Type: application/json' \
  -d '{"key":"loyalty_tier","label":"Loyalty Tier","type":"string"}'
Example request
const response = await fetch('https://api.quicksms.com/v1/custom-fields', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "key": "loyalty_tier",
    "label": "Loyalty Tier",
    "type": "string"
}),
});
const data = await response.json();
Example request
import requests

response = requests.post(
    'https://api.quicksms.com/v1/custom-fields',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
    json={
    "key": "loyalty_tier",
    "label": "Loyalty Tier",
    "type": "string"
},
)
data = response.json()
Response · 201
{
    "data": {
        "id": "uuid",
        "key": "loyalty_tier",
        "label": "Loyalty Tier",
        "type": "string"
    }
}
PUT /v1/custom-fields/{id} custom-fields:write

Update a custom field

The key and type are immutable once created; only the label can be updated.

Parameters, examples and response
Example request
curl -X PUT 'https://api.quicksms.com/v1/custom-fields/uuid' \
  -H 'Authorization: Bearer cbk_test_your_token' \
  -H 'Content-Type: application/json' \
  -d '{"label":"Loyalty Tier (Updated)"}'
Example request
const response = await fetch('https://api.quicksms.com/v1/custom-fields/uuid', {
  method: 'PUT',
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "label": "Loyalty Tier (Updated)"
}),
});
const data = await response.json();
Example request
import requests

response = requests.put(
    'https://api.quicksms.com/v1/custom-fields/uuid',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
    json={
    "label": "Loyalty Tier (Updated)"
},
)
data = response.json()
Response · 200
{
    "data": {
        "id": "uuid",
        "label": "Loyalty Tier (Updated)"
    }
}
DELETE /v1/custom-fields/{id} custom-fields:write

Delete a custom field

Removes a custom field definition. Existing contact custom_data values for that key are not removed.

Parameters, examples and response
Example request
curl -X DELETE 'https://api.quicksms.com/v1/custom-fields/uuid' \
  -H 'Authorization: Bearer cbk_test_your_token'
Example request
const response = await fetch('https://api.quicksms.com/v1/custom-fields/uuid', {
  method: 'DELETE',
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
  },
});
const data = await response.json();
Example request
import requests

response = requests.delete(
    'https://api.quicksms.com/v1/custom-fields/uuid',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
)
data = response.json()
Response · 200
{
    "data": {
        "message": "Custom field deleted"
    }
}

Operations & diagnostics

Track asynchronous bulk operations, verify tokens, and manage the sandbox.

GET /v1/operations/{id} any scope

Get an asynchronous operation

Retrieve the status and, when complete, the result of an asynchronous by-filter operation. Requires only that the requesting token has any valid scope. Poll at 2-second intervals backing off to 30 seconds; operations are retained for 30 days.

Parameters, examples and response
NameInTypeRequiredDescription
id path string Required The operation_id returned by the initiating call.
Example request
curl 'https://api.quicksms.com/v1/operations/uuid' \
  -H 'Authorization: Bearer cbk_test_your_token'
Example request
const response = await fetch('https://api.quicksms.com/v1/operations/uuid', {
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
  },
});
const data = await response.json();
Example request
import requests

response = requests.get(
    'https://api.quicksms.com/v1/operations/uuid',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
)
data = response.json()
Response · 200
{
    "data": {
        "operation_id": "op_8a7f6b4e-…",
        "type": "contacts.bulk_add_tags_by_filter",
        "status": "completed",
        "created_at": "2026-06-08T10:23:45Z",
        "started_at": "2026-06-08T10:23:46Z",
        "completed_at": "2026-06-08T10:24:12Z",
        "progress": {
            "total": 50000,
            "completed": 50000,
            "failed": 0
        },
        "result": {
            "affectedCount": 50000,
            "tags_applied": [
                "UK Active"
            ]
        }
    }
}
GET /v1/ping none

Verify your token

Echoes back your token's name, scopes, access level and account. Requires no scopes and consumes only a normal rate-limit slot.

Parameters, examples and response
Example request
curl 'https://api.quicksms.com/v1/ping' \
  -H 'Authorization: Bearer cbk_test_your_token'
Example request
const response = await fetch('https://api.quicksms.com/v1/ping', {
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
  },
});
const data = await response.json();
Example request
import requests

response = requests.get(
    'https://api.quicksms.com/v1/ping',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
)
data = response.json()
Response · 200
{
    "data": {
        "status": "ok",
        "authenticated": true,
        "account_id": "9c8b1a2e-…",
        "token_name": "My Integration",
        "token_prefix": "cbk_a1b2",
        "access_level": "admin",
        "scopes": [
            "contacts:read",
            "contacts:write"
        ],
        "request_id": "0dd0a4d6-…",
        "server_time": "2026-06-05T10:30:00+01:00"
    }
}
POST /v1/sandbox/reset sandbox token

Reset the sandbox

Wipes all sandbox data: contacts, lists, tags, opt-outs, custom field definitions, segments and bulk operations. Sandbox tokens only; a live token gets 403 NOT_A_SANDBOX_TOKEN. Idempotent, calling again immediately returns total: 0.

Parameters, examples and response
Example request
curl -X POST 'https://api.quicksms.com/v1/sandbox/reset' \
  -H 'Authorization: Bearer cbk_test_your_token'
Example request
const response = await fetch('https://api.quicksms.com/v1/sandbox/reset', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer cbk_test_your_token',
  },
});
const data = await response.json();
Example request
import requests

response = requests.post(
    'https://api.quicksms.com/v1/sandbox/reset',
    headers={'Authorization': 'Bearer cbk_test_your_token'},
)
data = response.json()
Response · 200
{
    "data": {
        "deleted": {
            "contacts": 1247,
            "contact_lists": 12,
            "tags": 5,
            "opt_out_records": 38,
            "opt_out_lists": 2,
            "contact_custom_field_definitions": 4,
            "segments": 3,
            "bulk_operations": 18
        },
        "total": 1329,
        "reset_at": "2026-06-08T07:14:33+00:00"
    },
    "request_id": "fb417523-…"
}

All APIs Updated 21 July 2026 · View changelog →

APIs, history & migration

One platform: the Messaging API and the Rich RCS API, both current, live and fully supported peers, plus a deprecated earlier generation (the Legacy API and Inbound V1) kept documented for the integrations still on it. Rich RCS is opt-in on a separate endpoint; every existing integration continues to work with zero changes.

The APIs, named clearly

NameAddressStatus
QuickSMS Messaging API POST https://api.quicksms.com/v1/messages Current · Live · Fully supported First-class API for SMS and RCS text with smart routing, delivery receipts with GSM status codes, and every sending control. Not legacy, not being replaced. If you send SMS with QuickSMS today, this is your API.
QuickSMS Rich RCS API POST https://api.quicksms.com/v2/messages Current · Live · Fully supported First-class API and a peer to the Messaging API: rich text, cards, carousels, media, buttons, suggested replies and actions, rich fallback, retrieval and lifecycle reporting. Additional capability, not a replacement; nothing moves automatically.
Legacy API POST https://api.quicksms.com/rcs/v2/api Deprecated Supported only for existing legacy integrations. Documented below; plan to migrate. (Its URL contains v2 for historical reasons; the name is Legacy API.)
Contact Book API https://api.quicksms.com/v1 (contacts, lists, tags, opt-outs, custom fields) Current · Live · Fully supported A separate product sharing the same host through path-based routing, with its own credentials (cbk_) and E.164 number format; see the Contact Book reference.

URL paths contain version numbers for historical reasons; they do not indicate product generations. Treat /v1/ and /v2/ as literal addresses, nothing more. This documentation never uses version numbers as product names.

Messaging API and Rich RCS API side by side

The two surfaces coexist; which one accepts a message determines its request shape, status vocabulary and webhook payload. Nothing here is a migration checklist, it is a map of what differs:

ConceptMessaging API (/v1/messages)Rich RCS (/v2/messages)
Recipient fieldmobile_number (preferred) or MSISDN (alias), digits onlymobile_number (digits only). MSISDN is not accepted; it returns a validation error pointing to mobile_number
Message contentFlat msg fieldStructured content object (text, media, rich_card, carousel)
Identifierquicksms_idmessage_id
Status vocabularyUppercase (ACCEPTED, DELIVERED, …)Lowercase (queued, delivered, read, fallback statuses)
Delivery receiptDLR payload with GSM status_codemessage.status payload with normalised failure.reason
Rich contentNo (RCS text via smart_routing)Yes
Message retrievalNoGET /v2/messages/{message_id}
Per-message cost in the responseNo (see reporting)Yes
Health checkGET /v1/healthGET /v2/health (and GET /health); all unauthenticated

Rich RCS is opt-in on a separate endpoint; every existing integration continues to work with zero changes. Which webhook payload you receive is determined solely by which endpoint accepted the message, never by account settings or content.

What is shared

  • Your credentials: the same sk_live_ / sk_test_ keys work on both endpoints.
  • Idempotency, rate limits, retry behaviour, anti-flood, out-of-hours, country restrictions and sandbox rules behave identically.
  • Webhook transport and signing: one HMAC scheme, so one verification implementation covers both payload shapes.
  • Both on one connection: send SMS through the Messaging API and rich content through the Rich RCS endpoint at the same time.

Adding Rich RCS to an existing integration

This is adoption, not migration: your existing Messaging API traffic is untouched, and you add calls to the new endpoint only where you want rich content.

  1. Register an RCS agent (verification with Google typically takes 1 to 2 weeks).
  2. Call POST /v2/messages with mobile_number, your agent as sender, a structured content object and an explicit SMS fallback.
  3. Store message_id from the response; retrieve state any time with GET /v2/messages/{message_id}.
  4. Handle the message.status payload for these sends; your existing DLR handler keeps serving your Messaging API sends. Signature verification is identical.
  5. New validation applies only here: SENDER_NOT_RCS_AGENT, INVALID_CONTENT and content limits; nothing new applies to your existing calls.

Migrating from the Legacy API

The one real migration. Moving from rcs/v2/api to the Messaging API gives you API keys, idempotency, signed webhooks, sandbox mode and the full set of sending controls:

  1. Create an API key in API Connections and replace Basic username/password auth with Authorization: Bearer.
  2. Change the endpoint to POST /v1/messages; drop reqType, keep MSISDN, rename your message field to msg.
  3. Replace cid with customer_id (and gain metadata); replace Delurl with callback_url.
  4. Read quicksms_id and the uppercase statuses instead of numeric codes; handle the DLR payload with GSM status codes.
  5. Verify webhook signatures, new with this move, see signature verification.

Contact Book deprecation policy

There are no plans to ship a Contact Book v2 in 2026. When a v2 does ship, v1 will remain available for at least 12 months from the v2 general-availability date, and deprecation will be announced 90 days in advance through an email to your account's address, a Deprecation: HTTP header on v1 responses, and an entry in the changelog.

Where to find each API's reference

The complete field-by-field contracts live under API reference: the Messaging API (request, uppercase status vocabulary, DLR payload and GSM status codes) and the Rich RCS API. This page covers naming, the side-by-side comparison, and the two migration paths only.

Legacy API (deprecated)

The original QuickSMS integration, kept documented because we deliberately keep our API history visible. It remains in service for the integrations built on it, but it is deprecated: it predates API keys, idempotency and signed webhooks. If you maintain one of these integrations, plan the migration to the Messaging API.

Don't confuse /rcs/v2/api with /v2/messages. Both paths contain "v2", but the numbers are historical and do not indicate product generations. This deprecated Legacy API is /rcs/v2/api; the current, first-class Rich RCS API is /v2/messages. They are different products, not two versions of one.

  • Endpoint: POST https://api.quicksms.com/rcs/v2/api, authenticated with HTTP Basic (username and password, not API keys).
  • Required fields: reqType (BULK), MSISDN, msg, sender. Optional: multi (allow multipart above 160 characters), cid (your reference, returned with receipts), Delurl (delivery receipt URL).
  • Success response: 200 with {"reference": "42"}. Errors return numeric codes, for example {"Error": 206, "Description": "Invalid username or password"}; the range covers 200 to 210 for transport and validation failures and 220/221 for the API being disabled after repeated failed logins.
  • Delivery receipts: POSTed to your configured URL with reference, cid, MSISDN, deliverytime, status and substatus. Status values: 3 delivered, 2 undeliverable, 4 expired, 5 rejected. Respond with the literal text RECEIVED.
  • RCS seen receipts: POSTed when an RCS message is read, with reference, CID, MSISDN and seentime. Respond with RECEIVED.

Inbound V1 (deprecated)

The original inbound delivery, from the same platform generation as the Legacy API above and kept documented for the integrations still on it. It is deprecated: it predates HMAC signing, and every V1 capability now has a current successor, the Inbound V2 webhook for SMS and RCS agent inbound for RCS replies, taps, files and locations. No new V1 integrations should be created; existing ones keep working and no removal date is set. Migration steps: migrating from Inbound V1.

The V1 contract: QuickSMS checks each incoming message against your registered numbers and RCS senders (register numbers in Management › Numbers) and, on a match, POSTs JSON to your configured endpoint from the V1 platform (base https://api.quicksms.com/rcs/v2/api). Respond within about one second with the literal text RECEIVED.

InformationPOST fieldExample
Unique referenceID42
Number or sender the message came fromMSISDN447123456789
Message textMessageHello
Number the message was sent toNumber44789011829
Received date and timeDateRecieved2024-01-28 11:54:32
V1 inbound message · example
POST https://www.example.co.uk/incoming.php
Content-Type: application/json

{
  "MSISDN": "NotifyTest",
  "Message": "Testing",
  "Number": "44789100000",
  "DateRecieved": "2024-01-28 11:54:32",
  "ID": "MxJFwONIywR7yZysg7iAtDXQ"
}

The field is really spelled DateRecieved. Existing integrations depend on that exact spelling, so it is preserved as-is; do not "correct" it in your parser.

Changelog & coming soon

What shipped when, what's planned next, and the tooling that helps you get started faster.

Postman collection

The fastest way to explore the API without writing code first. The Run in Postman button imports a prepared QuickSMS workspace containing:

  • A ready-made request for every endpoint: messaging (SMS, RCS text, Rich RCS) and the full Contact Book.
  • Test and live environments with variables for your API key, Contact Book token, sender ID and a test recipient, so switching environment never means editing requests.
  • Worked examples for rich cards, carousels, suggestion chips and SMS fallback.
  • Example webhook payloads and a signature-verification pre-request script you can adapt.

Coming soon

Until the collection is published, the quickstart gets you to a first test send in a few minutes with copy-paste cURL, and every example on these pages is runnable as written.

Coming soon

CapabilityWhat it adds
RCS on the flat Inbound V2 payload RCS inbound is live today through the per-agent RCS agent inbound webhook (enveloped payload). Carrying "channel": "rcs" on the flat V2 SMS payload as well is under consideration.
Capability check Query whether a mobile number is RCS-reachable before sending, so you can choose the right channel and content per recipient.
Batch sending Up to 1,000 messages in a single API request, for campaigns and high-volume transactional messaging.
Media library Upload and reuse media by ID instead of public URLs.
Message templates & scheduling Pre-registered rich templates, and send_at for future delivery.

Changelog

21 July 2026 · Inbound V1 deprecated

  • With RCS agent inbound live, every Inbound V1 capability now has a current, HMAC-signed successor, so Inbound V1 is deprecated: existing integrations keep working with no removal date, but no new V1 integrations should be created. Its reference now lives with the platform history on APIs, history & migration, and a migration guide covers the move (SMS to Inbound V2; RCS to per-agent inbound).

21 July 2026 · Send response echoes mobile_number

  • The POST /v1/messages response (202 and recipient-level 422) now echoes the recipient as mobile_number, matching the request field. MSISDN is retained in the response as a duplicate key with the same value, so existing parsers keep working with no change. Read mobile_number in new integrations.

21 July 2026 · RCS inbound: replies to your RCS agent

Two-way conversational RCS. Inbound handling is configured per Live agent, with the same four destinations as SMS inbound (Inbox, opt-out handling, webhook, email forwarding), enabled independently.

  • Per-agent inbound configuration in Management › RCS Agents V2, with a show-once, rotatable webhook signing secret.
  • Enveloped webhook payload distinguishing every interaction via message_type: typed replies, suggested-reply chip taps (your payload echoed back as postback_data), button taps, photo/file replies (media_url link) and shared locations (latitude/longitude). A worked example for each interaction.
  • Account-level API-connection inbound webhooks also receive RCS messages, tagged "source": "api_connection".

16 July 2026 · Recipient field naming and rejection codes

  • POST /v1/messages accepts mobile_number as the preferred recipient field, converging naming across the platform. MSISDN remains fully supported as an alias for existing integrations, with no deprecation. Send one or the other; if both are present with different values the request is rejected.
  • Pending deploy Content-filter rejections will return a generic MESSAGE_REJECTED code and message on both messaging surfaces, replacing CONTENT_BLOCKED in the public API vocabulary. The specific reason stays visible to the account owner in the portal message logs. Effective on deploy.

15 July 2026 · Health endpoint and unified error envelope

  • Health endpoint added at GET /v1/health; the same check is available at GET /v2/health and GET /health, all without authentication.
  • Every error on the messaging and Rich RCS surface now returns the canonical JSON error envelope, including 404, 405, malformed-JSON 400 and masked 500 responses.

July 2026 · Rich RCS API launched as a new QuickSMS messaging capability

Rich RCS is opt-in on a separate endpoint; every existing integration continues to work with zero changes.

  • Additional endpoint POST /v2/messages with the structured content model: text, media, rich_card, carousel, addressed with mobile_number.
  • Buttons and suggestions: URL, webview, dial, location and calendar actions.
  • Explicit SMS fallback object with a mandatory SMS rendition.
  • GET /v2/messages/{id}, message retrieval for sends made on this endpoint.
  • Lowercase status vocabulary for these sends, including read and fallback_delivered, delivered as message.status webhooks with the failure.reason taxonomy.
  • RCS billing classification: rich cards and carousels bill as RCS Single, per-message cost in responses.

June 2026 · Contact Book API v1.1

  • Canonical base URL https://api.quicksms.com/v1; the previous URL continues to work as an alias.
  • Idempotency keys on all state-changing endpoints.
  • /ping diagnostic endpoint.
  • Error envelope standardised to {"error": {"type", "code", "message", "request_id", "errors"}}.

June 2026 · Contact Book API v1.0

  • Initial public release: 47 endpoints across contacts, tags, lists, opt-outs and custom fields.
  • Bearer-token authentication with per-resource scopes and optional IP allowlists.
  • Asynchronous by-filter bulk operations for changes to more than 1,000 contacts.
  • Cross-list opt-out status check endpoint.

March 2026 · The QuickSMS Messaging API

The current production API, at POST /v1/messages.

  • Bearer and Basic authentication, idempotency keys (24-hour cache), smart routing with automatic SMS fallback, sandbox mode with simulated delivery.
  • HMAC-SHA256-signed delivery receipts with GSM status codes, rate limit headers (X-RateLimit-*), anti-flood and out-of-hours controls, country restrictions with dialling-prefix matching.
  • Structured JSON error responses with request_id.
  • URL paths contain version numbers for historical reasons; they do not indicate product generations, see the APIs, named clearly.

January 2025 · Inbound messaging V1

  • Receive incoming SMS and RCS messages via webhook.
  • Registered number and RCS sender matching, delivery to your configured endpoint.

2024 · Legacy API

  • HTTP Basic authentication (username and password), RCS and SMS sending with timeout-based SMS fallback.
  • Delivery receipts with status and substatus codes, RCS seen receipts, custom reference support (cid).
  • Now deprecated; still in service for existing integrations, see Legacy API (deprecated).