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.
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.
SMS and RCS text through the Messaging API, with smart routing, expiry and metadata.
Messaging guide →Branded cards, carousels, images and interactive buttons on the Rich RCS API, with optional, controlled SMS fallback.
Rich content guide →Create and import contacts, organise lists and tags, and keep consent up to date.
Contacts guide →Signed delivery receipts for delivery, read, failure and fallback outcomes on every channel.
Delivery receipts →Full request and response reference for every endpoint, with runnable examples.
API reference →Receive SMS replies through Inbound V2 and RCS replies through per-agent RCS inbound, routed to your webhook, QuickSMS Inbox, email or opt-out processing.
Inbound messaging →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
- Contacts are your audience layer, who you can message, how they're organised, and what they've consented to.
- Messaging sends to a recipient over the channel you choose, with content the channel supports.
- Events tell you what happened next, delivered, read, failed, or fell back to SMS, with the payload for each endpoint clearly documented.
- Errors use consistent envelope principles, with API-specific rejection and delivery models documented separately.
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.
The Authorization header
All APIs use the same header format, over HTTPS only:
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:
| Behaviour | Test (sk_test_) | Live (sk_live_) |
|---|---|---|
| Handset delivery | Simulated, nothing is sent | Real |
| Credits | Not charged | Charged per message part |
| Recipient | Must be in your approved test allowlist, otherwise TEST_NUMBER_NOT_APPROVED | Any permitted destination |
| Sender | The default QuickSMS test sender, unless you have an approved registered sender or purchased number | Your approved registered sender |
| Appended wording | "This is a QuickSMS test message" is appended | None |
| Anti-flood | Bypassed | Applied |
| Out-of-hours | Bypassed | Applied |
| Delivery receipts | Simulated after 3 to 5 seconds | Network-derived |
| RCS read receipts | Simulated 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 in | Simulated outcome |
|---|---|
0000 | UNDELIVERED with GSM code 27, absent subscriber (Rich RCS payloads report failed) |
9999 | EXPIRED with GSM code 255 (Rich RCS payloads report expired) |
| Anything else | DELIVERED; 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.
| Resource | Read scope | Write scope |
|---|---|---|
| Contacts | contacts:read | contacts:write |
| Tags | tags:read | tags:write |
| Lists | lists:read | lists:write |
| Opt-outs | opt-outs:read | opt-outs:write |
| Custom fields | custom-fields:read | custom-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
| State | Behaviour |
|---|---|
| Draft | Created but not yet live; requests are rejected until the connection is activated. |
| Active | Works normally. |
| Suspended | Requests are rejected until the connection is reactivated. |
| Archived | Retired 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
| State | Behaviour | Reversible? |
|---|---|---|
| Active | Works normally. | Not applicable |
| Suspended | Returns 401 Unauthorized. Pause access without losing the token. | Yes, reactivate any time. |
| Revoked | Returns 401 Unauthorized permanently. | No, create a new token. |
| Expired | If 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:
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.
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.
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.
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.
Choose your environment
Not startedStay 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:
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.
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.
Review the response
Not started{
"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.
{
"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.
{
"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.
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 (queued → submitted →
delivered → read, 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.
Receive a reply
Not startedSend 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".
Move to production
Not startedBefore 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
429backoff, see Errors & rate limits. - You send an
Idempotency-Keyon 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-Keyon 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.statuspayload, 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
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:
{
"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
}
| Field | Required | Description |
|---|---|---|
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 type | Rules | Example | If 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
| Field | Limit |
|---|---|
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 title | Max 200 characters. |
| Rich card description | Max 2,000 characters. |
| Button and suggestion labels | Max 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.
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).
{
"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.
{
"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.
{
"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 created | RCS billing classification | Potential SMS fallback |
|---|---|---|
| Text, 160 characters or fewer | RCS Basic | Calculated using SMS encoding and message parts |
| Text, 161 characters or more | RCS Single | Calculated using SMS encoding and message parts |
| Rich card | RCS Single | Calculated using the fallback SMS text, encoding and message parts |
| Carousel | RCS Single | Calculated using the fallback SMS text, encoding and message parts |
| Standalone media (image or file, no card) | RCS Single | Calculated using the fallback SMS text, encoding and message parts |
| Text with suggested replies | RCS Basic to 160 characters, RCS Single 161 or more | Calculated using SMS encoding and message parts |
| Text with suggested actions | RCS Single | Calculated 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:
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
channelfield (Rich RCS payloads additionally carryfell_back_to_sms: true, and supportGET /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_atare populated only for RCS-delivered messages and arenullfor 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:
| Priority | Source | Description |
|---|---|---|
| 1 (highest) | Explicit fallback sender on the request | Smart routing: sms_failover_sender. Rich content: fallback.sender_id. |
| 2 | Your account's default sender | The registered sender marked as default. |
| 3 | First approved sender | The first approved sender on your account. |
| 4 (last resort) | Sanitised agent name | The 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.
// 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 created | RCS billing classification | Potential SMS fallback |
|---|---|---|
| Text, 160 characters or fewer | RCS Basic | Calculated using SMS encoding and message parts |
| Text, 161 characters or more | RCS Single | Calculated using SMS encoding and message parts |
| Rich card | RCS Single | Calculated using the fallback SMS text, encoding and message parts |
| Carousel | RCS Single | Calculated using the fallback SMS text, encoding and message parts |
| Standalone media (image or file, no card) | RCS Single | Calculated using the fallback SMS text, encoding and message parts |
| Text with suggested replies | RCS Basic to 160 characters, RCS Single 161 or more | Calculated using SMS encoding and message parts |
| Text with suggested actions | RCS Single | Calculated 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 length | Encoding | Billable parts |
|---|---|---|
| 160 characters | GSM-7 | 1 |
| 161 characters | GSM-7 | 2 |
| 306 characters | GSM-7 | 2 |
| 307 characters | GSM-7 | 3 |
| 70 characters | UCS-2 | 1 |
| 71 characters | UCS-2 | 2 |
| 134 characters | UCS-2 | 2 |
| 135 characters | UCS-2 | 3 |
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:
{
"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:
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.
| Text | Length | Encoding | Parts | Billed |
|---|---|---|---|---|
| The message above, exactly as written | 150 characters | GSM-7 | 1 | 1 × SMS rate |
| The same message with " 🎉" appended | 153 characters (the space is 1; an emoji counts as 2 in UCS-2) | UCS-2 | 3 (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
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:
| Type | What the recipient sees | RCS billing classification |
|---|---|---|
text | Plain text, optionally with suggestion chips | RCS Basic (up to 160 chars) or RCS Single (161+); suggested replies keep the length-based class, any suggested action makes it RCS Single |
media | A standalone image or file | RCS Single |
rich_card | Image, title, description and up to 4 buttons | RCS Single |
carousel | 2 to 10 swipeable cards | RCS Single |
See the full RCS billing classification below.
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 created | RCS billing classification | Potential SMS fallback |
|---|---|---|
| Text, 160 characters or fewer | RCS Basic | Calculated using SMS encoding and message parts |
| Text, 161 characters or more | RCS Single | Calculated using SMS encoding and message parts |
| Rich card | RCS Single | Calculated using the fallback SMS text, encoding and message parts |
| Carousel | RCS Single | Calculated using the fallback SMS text, encoding and message parts |
| Standalone media (image or file, no card) | RCS Single | Calculated using the fallback SMS text, encoding and message parts |
| Text with suggested replies | RCS Basic to 160 characters, RCS Single 161 or more | Calculated using SMS encoding and message parts |
| Text with suggested actions | RCS Single | Calculated 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": {
"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.
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": {
"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" }
]
}
}
- 1
media, the hero image. HTTPS, publicly reachable, retrieved and cached by QuickSMS. - 2
title, max 200 characters. - 3
description, max 2,000 characters. - 4
buttons, up to 4 per card, labels max 25 characters.
Rich card anatomy
| Field | Values | Default |
|---|---|---|
orientation | vertical | horizontal | vertical |
thumbnail_alignment | left | right (horizontal only) | left |
media_height | short | medium | tall | medium |
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": {
"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" }]
}
]
}
}
Recipients swipe between cards
Media
A standalone image or file, optionally with a thumbnail:
"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_height | Rendered height | Suggested image | Available on |
|---|---|---|---|
short | 112 DP | Wide landscape strip, around 7:3 (e.g. 1440 × 620 px) | Standalone cards and carousel cards |
medium | 168 DP | Landscape, around 2:1 (e.g. 1440 × 720 px) | Standalone cards and carousel cards |
tall | 264 DP | Landscape hero around 1,500 px wide, around 16:9 (e.g. 1440 × 810 px) | Standalone vertical cards only |
tallis not supported on carousel cards; carousels useshortormedium.tallis also not available onhorizontal-orientation cards, where the media sits beside the text.- Carousel
card_width(smallormedium, defaultmedium) 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
contentJSON.
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_type | On tap | Required fields |
|---|---|---|
open_url | Opens the URL in the browser | url |
open_url_webview | Opens the URL inside the conversation | url; optional webview_mode: full | half | tall |
dial | Opens the dialler pre-filled | phone_number (E.164, e.g. +442037408909) |
view_location | Opens the map at a point | latitude, longitude; optional label |
create_calendar_event | Opens the calendar pre-filled | start_time, end_time (ISO 8601), title; optional description |
reply | Sends a reply into the conversation | None, see note below |
share_location | Prompts the user to share their location | None, 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:
RCS-capable handset: the rich card, status can reach read
Non-RCS handset: your sms_body, status fallback_delivered
"fallback": {
"enabled": true,
"sms_body": "Your order #12345 has been dispatched. Track: https://example.com/track",
"sender_id": "MyBrand"
}
| Field | Required | Description |
|---|---|---|
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}reportfell_back_to_sms: true, with terminal statusfallback_deliveredorfallback_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 afailedmessage with afailure.reason; nothing is delivered.
Limitations
- Rich content requires an approved RCS agent as the
sender; anything else is rejected synchronously withSENDER_NOT_RCS_AGENT. Rich content never quietly converts to SMS at send time. - Rich content always routes via RCS;
smart_routingapplies totextcontent only. - Chip icons and styling are fixed by the recipient's messaging app and cannot be customised.
replyandshare_locationinteractions 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.
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.
| SMS | RCS Text | Rich RCS | |
|---|---|---|---|
| What it is | Plain text to any mobile, worldwide | The same text, delivered as RCS with read receipts where the handset supports it, SMS otherwise | Branded cards, carousels, media and interactive buttons in the native inbox |
| API | Messaging API · POST /v1/messages | Messaging API · POST /v1/messages with smart_routing | Rich RCS API · POST /v2/messages |
| Prerequisites | Approved sender ID | Approved RCS agent + SMS fallback sender | Approved RCS agent (+ fallback sender for the SMS rendition) |
| Reaches | Every mobile | Every mobile (RCS or SMS) | RCS-capable handsets; everyone else via your explicit SMS fallback |
| Read receipts | No | Yes, when delivered as RCS | Yes, when delivered as RCS |
| Delivery receipt | DLR with GSM status codes | DLR; GSM code on SMS fallback, read receipt on RCS | message.status payload with failure.reason |
| Billing | Per SMS message part | RCS text by length (RCS Basic to 160 chars, RCS Single 161+); SMS parts on fallback | RCS Single per message for rich cards, carousels and standalone media; SMS parts on fallback |
| Best for | One-time codes, alerts, universal reach | Everyday notifications with better engagement at SMS-grade reach | Branded 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 created | RCS billing classification | Potential SMS fallback |
|---|---|---|
| Text, 160 characters or fewer | RCS Basic | Calculated using SMS encoding and message parts |
| Text, 161 characters or more | RCS Single | Calculated using SMS encoding and message parts |
| Rich card | RCS Single | Calculated using the fallback SMS text, encoding and message parts |
| Carousel | RCS Single | Calculated using the fallback SMS text, encoding and message parts |
| Standalone media (image or file, no card) | RCS Single | Calculated using the fallback SMS text, encoding and message parts |
| Text with suggested replies | RCS Basic to 160 characters, RCS Single 161 or more | Calculated using SMS encoding and message parts |
| Text with suggested actions | RCS Single | Calculated 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.
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:
| Mode | API behaviour | Credits |
|---|---|---|
| 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 |
{
"messages": [{
"quicksms_id": "msg_80fac5c752941b27",
"status": "HELD",
"scheduled_send_at": "2026-07-15T08:00:00+00:00"
}]
}
{
"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:
| Mode | Behaviour |
|---|---|
off | No duplicate checking. |
enforce | Duplicates are rejected with code DUPLICATE_MESSAGE; the response includes the original message's ID. |
monitor | Duplicates are logged but the message is still sent. |
{
"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:
| Method | Description |
|---|---|
Use an sk_test_ key | Always forces sandbox mode. |
Set "sandbox": true | Forces sandbox behaviour for that request, with any key type. |
| Connection environment set to test | Configured on the API connection in the portal. |
| Behaviour | Production | Sandbox |
|---|---|---|
| Real SMS and RCS delivery | Yes | No |
| Credits deducted | Yes | No |
| Anti-flood checks | Active | Bypassed |
| Out-of-hours checks | Active | Bypassed |
| Delivery webhooks | Real carrier receipts | Simulated after 3 to 5 seconds |
| RCS read receipts | From the recipient's device | Simulated 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:
callback_urlsupplied on the individual message, if any.- 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 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:
- Obtain or select an inbound number in Management › Numbers.
- Enable the routing options you need on that number.
- Configure an inbound webhook where your systems need the message, and store the signing secret.
- Send from the inbound number and receive an SMS reply.
- Verify the webhook signature on the request you receive.
- Process the inbound message in your system.
- Opt-out keywords are handled automatically where you have enabled opt-out processing.
- 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 option | Kind | What it does | Best 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 option | Required to enable |
|---|---|
| QuickSMS Inbox | Toggle on. No other configuration. |
| Webhook | An HTTPS forwarding URL. The signing secret is generated automatically. |
| Email forwarding | One or more email addresses (semicolon-separated). Delivery mode defaults to Immediate. |
| Automatic opt-out processing | Select 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:
| Mode | What arrives | Timing |
|---|---|---|
| Immediate | One email per inbound message. | Within seconds of the message arriving. |
| Hourly digest | One 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 digest | One 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:
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.
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:
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')
);
}
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);
}
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)
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));
}
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);
}
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))
}
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_idif 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.
| Field | Type | Description |
|---|---|---|
from | string | The number that sent the message, in E.164 format (e.g. +447891658398). |
to | string | The 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. |
content | string | The text body of the SMS, UTF-8. |
channel | string | sms. 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_id | string | Unique identifier for the inbound message; use it for de-duplication. |
timestamp | string | ISO 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 onmessage_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:
| Field | Description |
|---|---|
data.from | The sender's number, E.164 (e.g. +447700900123). |
data.to | The name of the RCS agent that received the message (an agent name, not a number). |
data.content | The text of the reply; for a button tap, the button's visible label; for a shared location, the marker [RCS interaction: LOCATION]. |
data.channel | rcs. Distinguishes RCS replies from SMS. |
data.message_id | Unique ID for this inbound message; use it for de-duplication. |
data.timestamp | When the message was received (ISO 8601 with offset). |
data.message_type | text (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_type | Broader 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_data | For taps, the payload attached to the button when it was sent (the outbound reply action's payload field). null for typed replies. |
data.media_url | For 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_size | The 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.longitude | For 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_type | Where the signal is |
|---|---|---|
| Typed a free-text reply | text | content is what they wrote |
| Tapped a suggested reply chip | suggested_reply | content is the chip label; postback_data is your payload |
| Tapped a button (open URL, dial, calendar) | suggested_action | content is the button label; the handset performs the action locally |
| Sent a photo or file | file | media_url, media_name, media_content_type, media_size |
| Shared their location | location | latitude and longitude |
1 · A typed, free-text 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.
{
"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".
{
"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.
{
"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].
{
"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:
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.
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.
5. Keep consent up to date
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:
| Scale | Use | Behaviour |
|---|---|---|
| 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.
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 by | Payload you receive | Documented |
|---|---|---|
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:
| Capability | SMS | RCS Text | Rich RCS |
|---|---|---|---|
| Delivered result | Yes | Yes | Yes |
| Read receipt | No | Yes, on RCS delivery | Yes, on RCS delivery |
| GSM status code | Yes | On SMS fallback | On SMS fallback |
| RCS failure information | No | Limited | Yes, failure.reason |
| Actual delivered channel | SMS | RCS or SMS | RCS or SMS |
| SMS message parts | Yes | On fallback | On fallback |
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:
| Status | Where | Final? | Meaning |
|---|---|---|---|
ACCEPTED | Send response | No | Validated and queued; wait for the receipt |
HELD | Send response | No | Delayed by out-of-hours; released at scheduled_send_at |
REJECTED | Send response | Yes | Refused synchronously; no receipt follows |
PENDING | Receipt | No | Awaiting carrier confirmation |
DELIVERED | Receipt | Yes | Confirmed on the handset (GSM code 0) |
UNDELIVERED | Receipt | Yes | Delivery failed; the GSM code says why |
EXPIRED | Receipt | Yes | Validity period elapsed (GSM code 255) |
REJECTED | Receipt | Yes | Rejected by the carrier (e.g. GSM code 13) |
{
"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.
{ "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.
{
"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"
}
{
"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:
| Status | Final? | Meaning | Channels |
|---|---|---|---|
queued | No | Accepted and queued for dispatch | All |
submitted | No | Handed to the mobile network for delivery | All |
delivered | Yes | Confirmed on the handset | All |
read | Yes | Read by the recipient | RCS text, Rich RCS |
failed | Yes | Delivery failed, see the failure object | All |
expired | Yes | Validity period elapsed before delivery | All |
rejected | Yes | Rejected before delivery, see the failure object | All |
fallback_delivered | Yes | RCS not possible; the SMS fallback was delivered | Rich RCS |
fallback_failed | Yes | The SMS fallback was attempted and failed | Rich 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:
| Header | Value |
|---|---|
X-QuickSMS-Timestamp | Unix seconds when the webhook was sent; used in the signature and for replay protection. |
X-QuickSMS-Event | What the webhook carries, for example dlr for a Messaging API delivery receipt. |
X-QuickSMS-Delivery-Attempt | Attempt number, starting at 1; increments on each retry. |
X-QuickSMS-Signature | sha256=<hex>, the HMAC described under signature verification. |
{
"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_idandmetadataare echoed on every event, correlate without a lookup. - A
network.mcc_mncobject is included when mobile network information is available. - Read receipts arrive as a subsequent event with
status: "read"andread_atpopulated.
Failure reasons
Receipts for failed, expired and rejected outcomes carry a failure object explaining why:
"failure": {
"reason": "not_capable",
"description": "Destination not RCS capable"
}
failure.reason | Meaning | What to do |
|---|---|---|
not_capable | The recipient's device is not RCS-capable | Enable SMS fallback, or send as SMS |
not_reachable | No RCS route to the recipient | Enable SMS fallback, or send as SMS |
ttl_expired | Validity period elapsed before delivery | Retry with a longer expiry if still relevant |
carrier_rejected | Rejected by the recipient's mobile network | Check sender registration and content compliance for the destination |
delivery_rejected | Rejected during downstream delivery, after QuickSMS accepted the message | Usually transient, retry; contact support with the message_id if it persists |
content_unsupported | The content could not be rendered or accepted | Check media requirements and per-field limits |
handset_unreachable | The device was offline for the whole validity period | Retry later, or rely on SMS fallback |
unknown | Unmapped failure | Contact 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:
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')
);
}
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);
}
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)
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));
}
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);
}
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))
}
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:
| Attempt | Delay after the previous failure |
|---|---|
| 1 | Immediate |
| 2 | 30 seconds |
| 3 | 2 minutes |
| 4 | 10 minutes |
| 5 | 1 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
| Event | Fires when | Channels | Availability |
|---|---|---|---|
| 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.
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:
| Namespace | Where it appears | Examples |
|---|---|---|
| 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": {
"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."]
}
}
}
| Field | Description |
|---|---|
type | Lowercase semantic category (unauthorized, validation_error, rate_limit_exceeded, …). Stable, fall back to this when you don't recognise a specific code. |
code | UPPERCASE machine-readable code. Branch on this, never on message. |
message | Human-readable summary for logs. Wording may change, don't parse it or show it to end users verbatim. |
request_id | Unique per request. Quote it to support so we can find the request in our logs. |
errors | Validation failures only: per-field arrays of messages. |
param | Optional, the request parameter or header the error refers to. |
retry_after | 429 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:
{
"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,acceptedandrejectedcount 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_numberand lowercaserejected. - 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
| Status | Meaning | When you'll see it |
|---|---|---|
200 / 201 | OK / Created | Successful reads, updates, deletes / resource creation. |
202 | Accepted | Queued for asynchronous processing (message sends, by-filter bulk operations). |
400 | Bad request | Malformed JSON, or an invalid Idempotency-Key. |
401 | Unauthorized | Missing or invalid credentials; a Contact Book token that is suspended, revoked, expired or used from outside its permitted IP allowlist. |
403 | Forbidden | Insufficient permission or scope; a Messaging connection accessed from an IP outside its allowlist (IP_NOT_ALLOWED). |
404 | Not found | The resource doesn't exist, or doesn't belong to your account. |
409 | Conflict | Idempotency key reused with a different body; by-filter count drift. |
422 | Validation failed | The request was well-formed but a field is invalid, check errors. |
429 | Rate limited | Too many requests, wait retry_after seconds, then retry. |
500 | Server error | Our 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.
| Code | HTTP | Likely cause | How to resolve | Retry? |
|---|---|---|---|---|
UNAUTHORIZED | 401 | 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 |
FORBIDDEN | 403 | Missing scope, the response echoes required_scope and your token's available_scopes |
Create a credential with the required scope | Fix first |
IP_NOT_ALLOWED | 403 | 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_FOUND | 404 | Wrong ID, or the resource belongs to another account | Check the identifier; IDs are account-scoped | Fix first |
VALIDATION_ERROR | 422 | 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_FIELD | 400 | Messaging: a required field is absent; param names it |
Add the missing field | Fix first |
BAD_REQUEST | 400 | Messaging: the request body is not valid JSON | Validate the JSON format | Fix first |
INVALID_PHONE_NUMBER | 422 | Messaging: the mobile number is not valid international format | Digits only, no +, 7 to 15 digits, country code first |
Fix first |
ACCOUNT_INACTIVE | 403 | Messaging: the account or connection cannot send | Check the connection state in API Connections, or contact support | Fix first |
AUTH_RATE_LIMITED | 429 | 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_CONTENT | 422 | 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_LONG | 422 | A text field exceeds its limit | See per-field limits under content types | Fix first |
IDEMPOTENCY_CONFLICT | 409 | Messaging: the same Idempotency-Key was sent with a different request body |
Generate a fresh key per distinct operation | Fix first |
IDEMPOTENCY_KEY_REUSED | 409 | 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_KEY | 400 | Empty or over-long key | Use up to 128 ASCII characters, a UUID works well | Fix first |
CONFLICT | 409 | By-filter expected_count drifted beyond tolerance |
Refresh the count and retry | Yes, after refresh |
RATE_LIMITED | 429 | Too many requests in the window | Wait retry_after seconds, then retry |
Yes, after waiting |
INTERNAL_ERROR | 500 | 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.
| Code | Meaning | How 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:
| Stage | What 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 codes | INVALID_PHONE_NUMBER, CONTENT_TOO_LONG (over 1,530 chars), MISSING_FIELD, BAD_REQUEST, plus the shared auth and rate-limit codes above |
| Idempotency conflict | IDEMPOTENCY_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 |
| Stage | What 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 codes | Same as SMS: INVALID_PHONE_NUMBER, CONTENT_TOO_LONG, MISSING_FIELD, BAD_REQUEST, plus the shared codes above |
| Idempotency conflict | IDEMPOTENCY_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 |
| Stage | What 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 codes | INVALID_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 conflict | IDEMPOTENCY_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.
| Surface | Tier | Limit |
|---|---|---|
| Messaging API | Standard | 1,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 API | Standard | 300 requests / minute. |
| Contact Book API | Bulk import endpoints | 60 requests / minute. |
The messaging API reports your live position on every response:
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests per minute for this connection. |
X-RateLimit-Remaining | Requests remaining in the current window. |
X-RateLimit-Reset | Unix timestamp when the window resets. |
Retry-After | On 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:
| Status | Retry? | Strategy |
|---|---|---|
429 | Yes | Wait the Retry-After value, then retry. |
500 | Yes | Retry with exponential backoff. |
503 | Yes | Service degraded; retry with backoff. |
400 to 422 | No | The request is the problem; fix it before resending. |
Recommended backoff, with jitter so concurrent clients don't retry in lockstep:
| Attempt | Base delay | With jitter |
|---|---|---|
| 1 | 1 second | 0.8 to 1.2 s |
| 2 | 2 seconds | 1.6 to 2.4 s |
| 3 | 4 seconds | 3.2 to 4.8 s |
| 4 | 8 seconds | 6.4 to 9.6 s |
| 5 | 16 seconds | 12.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:
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');
}
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')
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');
}
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");
}
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: trueheader, messaging returns the cached202. - Same key, different body:
409, protecting you from racing two different operations under one key. Messaging returnsIDEMPOTENCY_CONFLICT; the Contact Book returnsIDEMPOTENCY_KEY_REUSED. - Reads: keys are ignored on GET requests. A key longer than 128 characters is rejected with a validation error.
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
| Practice | Detail |
|---|---|
| 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
| Practice | Detail |
|---|---|
| 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
| Practice | Detail |
|---|---|
| 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. |
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.
- Check the synchronous response. A non-2xx status or a
REJECTEDentry inmessages[]means the request itself failed; branch on theerror.codeusing the rejection code catalogue. A rejected message gets no ID and no receipt. - 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 withfailure.reasonon the Rich RCS API.PENDINGis 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 acbk_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 theRetry-Afterheader 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_routingand all rich content require your approved RCS agent name as thesender; 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, sendmobile_number(preferred) orMSISDN(alias), not both with different values. OnPOST /v2/messages, onlymobile_numberis 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
0000simulatesUNDELIVERED(GSM 27) and9999simulatesEXPIRED(GSM 255). - The message is HELD: a sending control such as an out-of-hours window is holding it, with
scheduled_send_atin the response. Credits are reserved; it sends when the window opens. See sending controls. - It expired: delivery is attempted for
expiryseconds (default 48 hours), then the message endsEXPIREDwith 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;
PENDINGmeans 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
encodingandmessage_parts; you are billed onmessage_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
channelfield 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.reasonif 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_urloverrides 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_body1,530 (CONTENT_TOO_LONGnames 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
sendermust 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_idfrom 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.
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.
| Surface | Address | Credential |
|---|---|---|
| Messaging API (SMS, RCS text) | POST https://api.quicksms.com/v1/messages | sk_live_ / sk_test_ |
| QuickSMS Rich RCS API (opt-in) | POST https://api.quicksms.com/v2/messages | sk_live_ / sk_test_ |
| Contact Book (separate product) | https://api.quicksms.com/v1 | cbk_ / 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.
| Concept | Messaging 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 identifier | quicksms_id | message_id |
| Message text | msg | content.text (structured content object) |
| Send status vocabulary | UPPERCASE (ACCEPTED, DELIVERED, REJECTED) | lowercase (queued, delivered, read, failed) |
| Units in the response | message_parts (SMS segments) | cost (per-message) |
| Delivery outcome detail | GSM status codes | failure.reason taxonomy |
Messaging API
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
| Field | Required | Description |
|---|---|---|
mobile_number | Required | The 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. |
msg | Required | Message text, 1 to 1,530 characters. |
sender | Required | Approved sender ID, or your RCS agent name when smart_routing is enabled. |
smart_routing | Optional | Deliver via RCS with automatic SMS fallback. Default false. Requires an approved RCS agent as the sender. |
sms_failover_sender | Optional | SMS sender ID to use if an RCS message falls back to SMS, see the fallback sender resolution order. |
expiry | Optional | Validity in seconds. Min 60, max 259,200 (72 h), default 172,800 (48 h). |
customer_id | Optional | Your reference, max 128 chars. Echoed in DLR webhooks. |
metadata | Optional | Key-value pairs, max 10 keys, values max 256 chars. Echoed in DLR webhooks. |
sandbox | Optional | Force sandbox mode on a live key. Default false. |
callback_url | Optional | Per-message DLR webhook URL, max 2,048 chars. |
{
"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:
| Status | Meaning | Next step |
|---|---|---|
ACCEPTED | Queued for delivery | Wait for the DLR webhook |
HELD | Delayed by an out-of-hours rule | Dispatched automatically at scheduled_send_at; credits are reserved immediately |
REJECTED | Failed validation, never sent | Check error.code and fix; no webhook follows |
And in delivery receipts:
| Status | Final? | Meaning |
|---|---|---|
PENDING | No | Awaiting carrier confirmation |
DELIVERED | Yes | Confirmed delivered to the handset |
UNDELIVERED | Yes | Delivery failed, for example the phone was off |
EXPIRED | Yes | Validity period elapsed before delivery |
REJECTED | Yes | Rejected 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:
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" }
}
| Field | Type | Description |
|---|---|---|
quicksms_id | string | The message ID from the send response. |
customer_id | string or null | Your reference, if provided. |
channel | string | The channel actually used, SMS or RCS. May differ from the original request if RCS fell back to SMS. |
message_status | string | Final delivery status from the table above. |
status_code | integer | GSM status code, see below. |
message_parts | integer | Segment count. |
read_receipt | boolean or null | true if read (RCS only), null for SMS. |
read_receipt_timestamp | string or null | ISO 8601 read time (RCS only). |
done_time | string | ISO 8601 time the message reached its final status. |
metadata | object or null | Your 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
| Code | Meaning |
|---|---|
0 | Delivered successfully |
1 | Invalid destination number |
13 | Rejected by mobile network |
27 | Absent subscriber (phone off or out of range) |
32 | Network error |
255 | Unknown or expired |
These codes are part of the current SMS delivery model, they are not legacy.
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.
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
| Header | Required | Description |
|---|---|---|
Authorization | Required | Bearer sk_… |
Content-Type | Required | application/json |
Idempotency-Key | Optional | Makes retries safe; cached 24 hours. Details. |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
mobile_number | string | 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. The Contact Book API differs: it stores and returns numbers in E.164 with a leading +. |
sender | string | Required | SMS: an approved SMS sender ID. RCS text (smart_routing) and rich content: your approved RCS agent name (max 25 chars). |
content | object | Required | A type (text | media | rich_card | carousel) plus the matching sub-object. Content types. |
fallback | object | Optional | SMS fallback behaviour for rich content. Fallback. |
smart_routing | boolean | Optional | text content only: deliver via RCS where possible, SMS otherwise. Requires an approved RCS agent as the sender. Rich content always routes via RCS. |
expiry | integer | Optional | Validity in seconds. Min 60, max 259,200. Default 172,800 (48 h). |
customer_id | string | Optional | Your reference, max 128 chars. Returned in webhooks. |
metadata | object | Optional | Key-value pairs; max 10 keys, values max 256 chars. Returned in webhooks. |
sandbox | boolean | Optional | Force test behaviour on a live key. Default false. |
callback_url | string | Optional | Per-message webhook URL, max 2,048 chars. |
{
"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.
Retrieve a message
Current status and timestamps for a previously sent message. Webhooks are preferred; use this to reconcile.
{
"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 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.
List contacts
Paginated list of contacts with filtering, search and sorting. Returns up to 1000 per page.
Parameters, examples and response
| Name | In | Type | Required | Description |
|---|---|---|---|---|
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). |
curl 'https://api.quicksms.com/v1/contacts?per_page=25&status=active' \
-H 'Authorization: Bearer cbk_test_your_token'
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();
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()
{
"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 a contact
Returns a single contact with its tags, lists and custom data.
Parameters, examples and response
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id |
path | uuid | Required | Contact UUID. |
curl 'https://api.quicksms.com/v1/contacts/uuid' \
-H 'Authorization: Bearer cbk_test_your_token'
const response = await fetch('https://api.quicksms.com/v1/contacts/uuid', {
headers: {
Authorization: 'Bearer cbk_test_your_token',
},
});
const data = await response.json();
import requests
response = requests.get(
'https://api.quicksms.com/v1/contacts/uuid',
headers={'Authorization': 'Bearer cbk_test_your_token'},
)
data = response.json()
{
"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"
}
}
Create a contact
Creates a new contact. Only mobile_number is required.
Parameters, examples and response
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"}}'
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();
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()
{
"data": {
"id": "9c8b1a2e-…",
"mobile_number": "+447700900123",
"first_name": "Alice",
"status": "active",
"created_at": "2026-06-03T07:30:00Z"
}
}
Update a contact
Updates a contact. Only supplied fields are changed.
Parameters, examples and response
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"}'
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();
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()
{
"data": {
"id": "9c8b1a2e-…",
"first_name": "Alicia",
"email": "alicia@example.com"
}
}
Delete a contact
Soft-deletes a contact. List memberships are preserved for restoration.
Parameters, examples and response
curl -X DELETE 'https://api.quicksms.com/v1/contacts/uuid' \
-H 'Authorization: Bearer cbk_test_your_token'
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();
import requests
response = requests.delete(
'https://api.quicksms.com/v1/contacts/uuid',
headers={'Authorization': 'Bearer cbk_test_your_token'},
)
data = response.json()
{
"data": {
"message": "Contact deleted"
}
}
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
curl 'https://api.quicksms.com/v1/contacts/uuid/mobile' \
-H 'Authorization: Bearer cbk_test_your_token'
const response = await fetch('https://api.quicksms.com/v1/contacts/uuid/mobile', {
headers: {
Authorization: 'Bearer cbk_test_your_token',
},
});
const data = await response.json();
import requests
response = requests.get(
'https://api.quicksms.com/v1/contacts/uuid/mobile',
headers={'Authorization': 'Bearer cbk_test_your_token'},
)
data = response.json()
{
"mobile_number": "+447700900123"
}
Contact counts
Lightweight KPI counts. Accepts the same filter params as /contacts.
Parameters, examples and response
curl 'https://api.quicksms.com/v1/contacts/stats' \
-H 'Authorization: Bearer cbk_test_your_token'
const response = await fetch('https://api.quicksms.com/v1/contacts/stats', {
headers: {
Authorization: 'Bearer cbk_test_your_token',
},
});
const data = await response.json();
import requests
response = requests.get(
'https://api.quicksms.com/v1/contacts/stats',
headers={'Authorization': 'Bearer cbk_test_your_token'},
)
data = response.json()
{
"total": 152,
"active": 148,
"opted_out": 4,
"recent_7d": 12
}
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
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"}]}'
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();
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()
{
"data": {
"inserted": 2,
"skipped": 0,
"total": 2
}
}
Add tags to many contacts
Adds tags to up to 1000 contacts. Missing tags are created. Idempotent.
Parameters, examples and response
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"]}'
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();
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()
{
"data": {
"message": "Added tag(s) \"VIP\" to 2 contact(s)",
"affectedCount": 2
}
}
Remove tags from many contacts
Removes tags from up to 1000 contacts. Tags that don't resolve are silently ignored.
Parameters, examples and response
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"]}'
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();
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()
{
"data": {
"affectedCount": 1
}
}
Add contacts to a list
Adds up to 1000 contacts to a list, addressed by list name. Idempotent.
Parameters, examples and response
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"}'
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();
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()
{
"data": {
"affectedCount": 1
}
}
Remove contacts from a list
Removes up to 1000 contacts from a list, addressed by list name.
Parameters, examples and response
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"}'
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();
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()
{
"data": {
"affectedCount": 1
}
}
Opt out many contacts
Adds the mobile numbers of up to 1000 contacts to an opt-out list. Idempotent.
Parameters, examples and response
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"}'
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();
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()
{
"data": {
"affectedCount": 1
}
}
Remove from opt-out
Removes the mobile numbers of up to 1000 contacts from a specific opt-out list.
Parameters, examples and response
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"}'
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();
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()
{
"data": {
"affectedCount": 1
}
}
Bulk soft-delete contacts
Soft-deletes up to 1000 contacts. List memberships preserved for restoration.
Parameters, examples and response
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"]}'
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();
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()
{
"data": {
"affectedCount": 2
}
}
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
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"]}'
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();
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()
{
"data": {
"operation_id": "op-uuid",
"status": "queued",
"total_items": 50000
}
}
Remove tags by filter (async)
Asynchronous variant of /contacts/bulk/remove-tags. Tags that don't resolve are skipped.
Parameters, examples and response
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"]}'
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();
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()
{
"data": {
"operation_id": "op-uuid",
"status": "queued",
"total_items": 12000
}
}
Add to list by filter (async)
Asynchronous variant of /contacts/bulk/add-to-list.
Parameters, examples and response
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"}'
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();
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()
{
"data": {
"operation_id": "op-uuid",
"status": "queued",
"total_items": 30000
}
}
Remove from list by filter (async)
Asynchronous variant of /contacts/bulk/remove-from-list.
Parameters, examples and response
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"}'
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();
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()
{
"data": {
"operation_id": "op-uuid",
"status": "queued",
"total_items": 5000
}
}
Soft-delete by filter (async)
Asynchronous variant of /contacts/bulk/delete, with the same expected_count drift-tolerance check.
Parameters, examples and response
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}'
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();
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()
{
"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.
List tags
Returns all tags for the account, alphabetised.
Parameters, examples and response
| Name | In | Type | Required | Description |
|---|---|---|---|---|
search |
query | string | Optional | Substring match on tag name. |
curl 'https://api.quicksms.com/v1/tags' \
-H 'Authorization: Bearer cbk_test_your_token'
const response = await fetch('https://api.quicksms.com/v1/tags', {
headers: {
Authorization: 'Bearer cbk_test_your_token',
},
});
const data = await response.json();
import requests
response = requests.get(
'https://api.quicksms.com/v1/tags',
headers={'Authorization': 'Bearer cbk_test_your_token'},
)
data = response.json()
{
"data": [
{
"id": "uuid",
"name": "VIP",
"color": "#6f42c1",
"contact_count": 42
},
{
"id": "uuid",
"name": "Newsletter",
"color": "#0d6efd",
"contact_count": 1024
}
]
}
Create a tag
Creates a new tag. Names must be unique within the account.
Parameters, examples and response
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"}'
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();
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()
{
"data": {
"id": "uuid",
"name": "VIP",
"color": "#6f42c1",
"contact_count": 0
}
}
Update a tag
Updates a tag's name and/or colour.
Parameters, examples and response
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"}'
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();
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()
{
"data": {
"id": "uuid",
"name": "VVIP",
"color": "#dc2626"
}
}
Delete a tag
Deletes a tag and removes all contact-tag associations.
Parameters, examples and response
curl -X DELETE 'https://api.quicksms.com/v1/tags/uuid' \
-H 'Authorization: Bearer cbk_test_your_token'
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();
import requests
response = requests.delete(
'https://api.quicksms.com/v1/tags/uuid',
headers={'Authorization': 'Bearer cbk_test_your_token'},
)
data = response.json()
{
"data": {
"message": "Tag deleted"
}
}
Lists
Manage contact lists (static membership) and their members.
List all lists
Returns all lists for the account.
Parameters, examples and response
| Name | In | Type | Required | Description |
|---|---|---|---|---|
search |
query | string | Optional | Substring match on list name. |
type |
query | enum | Optional | static or dynamic. |
curl 'https://api.quicksms.com/v1/lists' \
-H 'Authorization: Bearer cbk_test_your_token'
const response = await fetch('https://api.quicksms.com/v1/lists', {
headers: {
Authorization: 'Bearer cbk_test_your_token',
},
});
const data = await response.json();
import requests
response = requests.get(
'https://api.quicksms.com/v1/lists',
headers={'Authorization': 'Bearer cbk_test_your_token'},
)
data = response.json()
{
"data": [
{
"id": "uuid",
"name": "Newsletter Subscribers",
"description": "Opted-in subscribers",
"type": "static",
"contact_count": 1024,
"created_at": "2026-05-01T10:00:00Z"
}
]
}
Create a list
Creates a new list, optionally seeded with initial contact_ids.
Parameters, examples and response
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"]}'
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();
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()
{
"data": {
"id": "uuid",
"name": "VIP Customers",
"contact_count": 2,
"type": "static"
}
}
Update a list
Updates a list's metadata. Contents are managed through the members endpoints.
Parameters, examples and response
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"}'
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();
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()
{
"data": {
"id": "uuid",
"name": "VIP Customers (2026)"
}
}
Delete a list
Deletes a list and removes all contact-list associations. The contacts themselves are not affected.
Parameters, examples and response
curl -X DELETE 'https://api.quicksms.com/v1/lists/uuid' \
-H 'Authorization: Bearer cbk_test_your_token'
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();
import requests
response = requests.delete(
'https://api.quicksms.com/v1/lists/uuid',
headers={'Authorization': 'Bearer cbk_test_your_token'},
)
data = response.json()
{
"data": {
"message": "List deleted"
}
}
Add members to a list
Adds contacts to a list. Idempotent.
Parameters, examples and response
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"]}'
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();
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()
{
"data": {
"added": 2
}
}
Remove members from a list
Removes contacts from a list. Returns the actual delete count.
Parameters, examples and response
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"]}'
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();
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()
{
"data": {
"removed": 1
}
}
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
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"}]}'
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();
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()
{
"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.
List opt-out lists
Returns all opt-out lists for the account. The master opt-out list always appears first.
Parameters, examples and response
| Name | In | Type | Required | Description |
|---|---|---|---|---|
search |
query | string | Optional | Substring match on list name. |
curl 'https://api.quicksms.com/v1/opt-out-lists' \
-H 'Authorization: Bearer cbk_test_your_token'
const response = await fetch('https://api.quicksms.com/v1/opt-out-lists', {
headers: {
Authorization: 'Bearer cbk_test_your_token',
},
});
const data = await response.json();
import requests
response = requests.get(
'https://api.quicksms.com/v1/opt-out-lists',
headers={'Authorization': 'Bearer cbk_test_your_token'},
)
data = response.json()
{
"data": [
{
"id": "uuid",
"name": "Master Opt-Out",
"is_master": true,
"count": 4
},
{
"id": "uuid",
"name": "Campaign X Opt-Outs",
"is_master": false,
"count": 12
}
]
}
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
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"}'
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();
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()
{
"data": {
"id": "uuid",
"name": "Campaign X Opt-Outs",
"is_master": false,
"count": 0
}
}
Update an opt-out list
Updates name and/or description.
Parameters, examples and response
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)"}'
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();
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()
{
"data": {
"id": "uuid",
"name": "Campaign X Opt-Outs (closed)"
}
}
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
curl -X DELETE 'https://api.quicksms.com/v1/opt-out-lists/uuid' \
-H 'Authorization: Bearer cbk_test_your_token'
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();
import requests
response = requests.delete(
'https://api.quicksms.com/v1/opt-out-lists/uuid',
headers={'Authorization': 'Bearer cbk_test_your_token'},
)
data = response.json()
{
"data": {
"message": "Opt-out list deleted"
}
}
List records in an opt-out list
Paginated list of opt-out records within a specific opt-out list.
Parameters, examples and response
| Name | In | Type | Required | Description |
|---|---|---|---|---|
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. |
curl 'https://api.quicksms.com/v1/opt-out-lists/list-uuid/records' \
-H 'Authorization: Bearer cbk_test_your_token'
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();
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()
{
"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
}
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
curl 'https://api.quicksms.com/v1/opt-out-records?opt_out_list_id=list-uuid' \
-H 'Authorization: Bearer cbk_test_your_token'
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();
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()
{
"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
}
Add a number to an opt-out list
Adds a single mobile number to an opt-out list. Idempotent.
Parameters, examples and response
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"}'
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();
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()
{
"data": {
"id": "uuid",
"mobile_number": "+447700900099",
"source": "manual"
}
}
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
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"}]}'
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();
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()
{
"data": {
"inserted": 2,
"skipped": 0,
"total": 2
}
}
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
curl -X DELETE 'https://api.quicksms.com/v1/opt-out-records/uuid' \
-H 'Authorization: Bearer cbk_test_your_token'
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();
import requests
response = requests.delete(
'https://api.quicksms.com/v1/opt-out-records/uuid',
headers={'Authorization': 'Bearer cbk_test_your_token'},
)
data = response.json()
{
"data": {
"message": "Opt-out record deleted"
}
}
Reveal opt-out mobile
Returns the unmasked mobile number for an opt-out record.
Parameters, examples and response
curl 'https://api.quicksms.com/v1/opt-out-records/uuid/mobile' \
-H 'Authorization: Bearer cbk_test_your_token'
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();
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()
{
"mobile_number": "+447700900099"
}
Move records between opt-out lists
Reassigns one or more opt-out records from one list to another.
Parameters, examples and response
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"}'
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();
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()
{
"data": {
"moved": 2
}
}
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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
mobile_number |
query | string | Required | Mobile number in E.164 format (e.g. +447700900099). URL-encode the leading + as %2B. |
curl 'https://api.quicksms.com/v1/opt-outs/check?mobile_number=%2B447700900099' \
-H 'Authorization: Bearer cbk_test_your_token'
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();
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()
{
"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.
List custom field definitions
Returns all custom field definitions for the account.
Parameters, examples and response
curl 'https://api.quicksms.com/v1/custom-fields' \
-H 'Authorization: Bearer cbk_test_your_token'
const response = await fetch('https://api.quicksms.com/v1/custom-fields', {
headers: {
Authorization: 'Bearer cbk_test_your_token',
},
});
const data = await response.json();
import requests
response = requests.get(
'https://api.quicksms.com/v1/custom-fields',
headers={'Authorization': 'Bearer cbk_test_your_token'},
)
data = response.json()
{
"data": [
{
"id": "uuid",
"key": "loyalty_tier",
"label": "Loyalty Tier",
"type": "string"
},
{
"id": "uuid",
"key": "join_date",
"label": "Join Date",
"type": "date"
}
]
}
Create a custom field
The key is used in contact custom_data; the label is the display name.
Parameters, examples and response
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"}'
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();
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()
{
"data": {
"id": "uuid",
"key": "loyalty_tier",
"label": "Loyalty Tier",
"type": "string"
}
}
Update a custom field
The key and type are immutable once created; only the label can be updated.
Parameters, examples and response
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)"}'
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();
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()
{
"data": {
"id": "uuid",
"label": "Loyalty Tier (Updated)"
}
}
Delete a custom field
Removes a custom field definition. Existing contact custom_data values for that key are not removed.
Parameters, examples and response
curl -X DELETE 'https://api.quicksms.com/v1/custom-fields/uuid' \
-H 'Authorization: Bearer cbk_test_your_token'
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();
import requests
response = requests.delete(
'https://api.quicksms.com/v1/custom-fields/uuid',
headers={'Authorization': 'Bearer cbk_test_your_token'},
)
data = response.json()
{
"data": {
"message": "Custom field deleted"
}
}
Operations & diagnostics
Track asynchronous bulk operations, verify tokens, and manage the sandbox.
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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id |
path | string | Required | The operation_id returned by the initiating call. |
curl 'https://api.quicksms.com/v1/operations/uuid' \
-H 'Authorization: Bearer cbk_test_your_token'
const response = await fetch('https://api.quicksms.com/v1/operations/uuid', {
headers: {
Authorization: 'Bearer cbk_test_your_token',
},
});
const data = await response.json();
import requests
response = requests.get(
'https://api.quicksms.com/v1/operations/uuid',
headers={'Authorization': 'Bearer cbk_test_your_token'},
)
data = response.json()
{
"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"
]
}
}
}
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
curl 'https://api.quicksms.com/v1/ping' \
-H 'Authorization: Bearer cbk_test_your_token'
const response = await fetch('https://api.quicksms.com/v1/ping', {
headers: {
Authorization: 'Bearer cbk_test_your_token',
},
});
const data = await response.json();
import requests
response = requests.get(
'https://api.quicksms.com/v1/ping',
headers={'Authorization': 'Bearer cbk_test_your_token'},
)
data = response.json()
{
"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"
}
}
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
curl -X POST 'https://api.quicksms.com/v1/sandbox/reset' \
-H 'Authorization: Bearer cbk_test_your_token'
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();
import requests
response = requests.post(
'https://api.quicksms.com/v1/sandbox/reset',
headers={'Authorization': 'Bearer cbk_test_your_token'},
)
data = response.json()
{
"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-…"
}
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
| Name | Address | Status |
|---|---|---|
| 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:
| Concept | Messaging API (/v1/messages) | Rich RCS (/v2/messages) |
|---|---|---|
| Recipient field | mobile_number (preferred) or MSISDN (alias), digits only | mobile_number (digits only). MSISDN is not accepted; it returns a validation error pointing to mobile_number |
| Message content | Flat msg field | Structured content object (text, media, rich_card, carousel) |
| Identifier | quicksms_id | message_id |
| Status vocabulary | Uppercase (ACCEPTED, DELIVERED, …) | Lowercase (queued, delivered, read, fallback statuses) |
| Delivery receipt | DLR payload with GSM status_code | message.status payload with normalised failure.reason |
| Rich content | No (RCS text via smart_routing) | Yes |
| Message retrieval | No | GET /v2/messages/{message_id} |
| Per-message cost in the response | No (see reporting) | Yes |
| Health check | GET /v1/health | GET /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.
- Register an RCS agent (verification with Google typically takes 1 to 2 weeks).
- Call
POST /v2/messageswithmobile_number, your agent assender, a structuredcontentobject and an explicit SMS fallback. - Store
message_idfrom the response; retrieve state any time withGET /v2/messages/{message_id}. - Handle the message.status payload for these sends; your existing DLR handler keeps serving your Messaging API sends. Signature verification is identical.
- New validation applies only here:
SENDER_NOT_RCS_AGENT,INVALID_CONTENTand 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:
- Create an API key in API Connections and replace Basic username/password auth with
Authorization: Bearer. - Change the endpoint to
POST /v1/messages; dropreqType, keepMSISDN, rename your message field tomsg. - Replace
cidwithcustomer_id(and gainmetadata); replaceDelurlwithcallback_url. - Read
quicksms_idand the uppercase statuses instead of numeric codes; handle the DLR payload with GSM status codes. - 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:
200with{"reference": "42"}. Errors return numeric codes, for example{"Error": 206, "Description": "Invalid username or password"}; the range covers200to210for transport and validation failures and220/221for the API being disabled after repeated failed logins. - Delivery receipts: POSTed to your configured URL with
reference,cid,MSISDN,deliverytime,statusandsubstatus. Status values:3delivered,2undeliverable,4expired,5rejected. Respond with the literal textRECEIVED. - RCS seen receipts: POSTed when an RCS message is read, with
reference,CID,MSISDNandseentime. Respond withRECEIVED.
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.
| Information | POST field | Example |
|---|---|---|
| Unique reference | ID | 42 |
| Number or sender the message came from | MSISDN | 447123456789 |
| Message text | Message | Hello |
| Number the message was sent to | Number | 44789011829 |
| Received date and time | DateRecieved | 2024-01-28 11:54:32 |
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
| Capability | What 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/messagesresponse (202 and recipient-level 422) now echoes the recipient asmobile_number, matching the request field.MSISDNis retained in the response as a duplicate key with the same value, so existing parsers keep working with no change. Readmobile_numberin 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 (yourpayloadechoed back aspostback_data), button taps, photo/file replies (media_urllink) 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/messagesacceptsmobile_numberas the preferred recipient field, converging naming across the platform.MSISDNremains 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_REJECTEDcode and message on both messaging surfaces, replacingCONTENT_BLOCKEDin 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 atGET /v2/healthandGET /health, all without authentication. - Every error on the messaging and Rich RCS surface now returns the canonical JSON error envelope, including
404,405, malformed-JSON400and masked500responses.
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/messageswith the structured content model:text,media,rich_card,carousel, addressed withmobile_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
readandfallback_delivered, delivered asmessage.statuswebhooks with thefailure.reasontaxonomy. - RCS billing classification: rich cards and carousels bill as RCS Single, per-message
costin 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.
/pingdiagnostic 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).