Quad State SMS API documentation
Base URL: https://api.quadstate.us. Content type: application/json. These docs are public. Customer API requests require a Bearer token.
Authentication & quick start
Base URL: https://api.quadstate.us. Quad State issues each customer an API token and assigns their SMS numbers. Tokens are scoped to that customer. New tokens contain 48 uppercase hexadecimal characters. GET /numbers lists only the numbers in that customer’s inventory, with enabled flags and channel capabilities; an empty inventory returns {"data":[]}. Keep them on your application server, and send them in the Authorization header. Request another token or revoke a compromised token through Quad State support.
curl https://api.quadstate.us/numbers \
-H "Authorization: Bearer $QUAD_STATE_SMS_TOKEN"
curl https://api.quadstate.us/messages \
-H "Authorization: Bearer $QUAD_STATE_SMS_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-123-notification-1" \
-d '{"from":"+12705551234","to":"+16185551234","text":"Your appointment is confirmed."}'Use your assigned number as from. US and Canadian numbers use +1XXXXXXXXXX; 10-digit input is also accepted and normalized. Text supports up to 1,600 characters. Long text can become multiple billable SMS segments. SMS sending and receiving are implemented. MMS, RCS, and iMessage are reserved channels and currently unavailable. Attachments are currently unavailable.
Send response
{
"id": "msg_…",
"object": "sms_message",
"direction": "outbound",
"channel": "sms",
"from": "+12705551234",
"to": "+16185551234",
"text": "Your appointment is confirmed.",
"status": "accepted",
"error_code": null,
"created_at": "2026-09-30T21:00:00Z",
"updated_at": "2026-09-30T21:00:01Z"
}Every send requires a unique Idempotency-Key (1–128 letters, digits, or ._:-). Retries with the same key and content return the original message without sending it again. A different payload with the same key returns 409. Keys remain reserved for the lifetime of the message record.
A first submission returns HTTP 202 with its current result; a replay returns HTTP 200 and Idempotency-Replayed: true. Always inspect status: accepted means accepted for sending; failed means rejected; submitting is in progress; unknown means the sending result could not be confirmed. Inbound messages have received status. Handset delivery receipts are not available currently. For an unknown result, keep the original key and check with support; a new key may send a duplicate.
Only send messages to recipients who have agreed to receive them. Received STOP, STOPALL, UNSUBSCRIBE, CANCEL, END, QUIT, REVOKE, and OPTOUT messages block further sending to that recipient from your customer account. START and UNSTOP remove that block. Your application remains responsible for consent records and approved confirmation replies. Read /messaging-policy for the active wording and keywords. Pending change requests do not affect keyword handling.
Number capabilities
Get capabilities for your assigned number, or read them in GET /numbers. Access requires your customer token; other customers' numbers return 404.
curl https://api.quadstate.us/numbers/+12705551234/capabilities \ -H "Authorization: Bearer $QUAD_STATE_SMS_TOKEN"
{
"number": "+12705551234",
"capabilities": {
"sms": {
"status": "available",
"send": {"status": "unavailable", "reason": "activation_pending"},
"receive": {"status": "available", "reason": null}
},
"mms": {
"status": "unavailable",
"send": {"status": "unavailable", "reason": "not_supported"},
"receive": {"status": "unavailable", "reason": "not_supported"}
},
"rcs": {
"status": "unavailable",
"send": {"status": "unavailable", "reason": "not_supported"},
"receive": {"status": "unavailable", "reason": "not_supported"}
},
"imessage": {
"status": "unavailable",
"send": {"status": "unavailable", "reason": "not_supported"},
"receive": {"status": "unavailable", "reason": "not_supported"}
}
}
}A channel's status is available if sending or receiving is available. Check the direction you need. Reasons: number_disabled, not_supported, not_enabled, or activation_pending. An available direction has a null reason. This reports implementation support and account provisioning; delivery results are reported on each message.
Receiving available; sending awaiting activation
Quad State can receive and store incoming SMS before outbound sending is activated. Incoming messages remain available through GET /messages and can be forwarded to your configured account or number webhook. Outbound SMS requires approved messaging registration and sending activation for the source number.
A receive-only number reports:
{
"sms": {
"status": "available",
"send": {"status": "unavailable", "reason": "activation_pending"},
"receive": {"status": "available", "reason": null}
}
}Check send and receive separately. The overall sms.status is available because at least one direction works. send_enabled is false in this state. activation_pending means Quad State has not activated sending for this number. Its activation object explains whether messaging registration or number activation is pending. Successful inbound SMS does not establish outbound activation. New sending requests return HTTP 503 with error code sending_unavailable and are not submitted for sending. Wait for Quad State to confirm activation, then retrieve capabilities again before sending.
An existing message with status accepted records a sending acknowledgment only. It does not confirm a delivery record, registration approval, or delivery to the recipient's phone. An accepted message can remain undelivered; check with Quad State if delivery is not confirmed.
Use the same POST /messages endpoint with "channel":"sms", "mms", "rcs", or "imessage". Omitting channel defaults to SMS. Reserved channels return 422 channel_unavailable until supported and provisioned. SMS sending awaiting activation returns 503 sending_unavailable. Unsupported requests are never sent as SMS. Message objects and new webhook event payloads include channel. Previously queued events keep their original signed body.
The existing enabled and send_enabled number fields remain available; send_enabled continues to describe SMS sending.
API endpoints
The customer endpoints below require your Bearer token, except /health. Send JSON bodies with Content-Type: application/json. Use the OpenAPI specification to generate an application client.
| Method | Endpoint | Purpose |
|---|---|---|
| GET | /account | Your account ID and name |
| GET | /numbers | Your assigned numbers and channel capabilities |
| GET | /numbers/{number} | One assigned number and its capabilities |
| GET | /numbers/{number}/capabilities | SMS, MMS, RCS, and iMessage availability |
| POST | /messages | Submit a message with an optional channel |
| GET | /messages | Message history; limit=1–100, before=message ID |
| GET | /messages/{id} | Retrieve a message |
| GET / PUT | /webhook | Read or set the account default receiving URL |
| GET / PUT | /numbers/{number}/webhook | Set a number override, use the account default, or pause a number |
| POST | /webhook/rotate-secret | Replace your signing secret |
| GET | /webhook/deliveries | Latest 100 webhook deliveries |
| POST | /webhook/deliveries/{id}/retry | Replay an event |
| GET | /messaging-policy | Read active opt-in, opt-out, and HELP wording and keywords |
| GET / POST | /messaging-policy/requests | List requests or submit proposed changes for Quad State review |
| GET | /messaging-policy/requests/{id} | Read a request, status, and review note |
| POST | /messaging-policy/requests/{id}/cancel | Cancel a pending request |
| GET | /health | Service health; no token required |
Message history returns {"data": [...], "next_cursor": "msg_…"}. Pass that cursor as before on the next page. A null cursor means there are no further results. No browser cross-origin access is enabled; call this API from your application's server.
Messaging policy and approval requests
Use GET /messaging-policy to read your account's active opt-in, opt-out, and HELP messages and keywords. Each approved policy has a revision. Revision 0 means no approved wording is configured; message values are null and standard keywords still work. Policies apply to all numbers on the account. Responses identify approval_status, the active values, and the number of pending requests.
To propose a change, send POST /messaging-policy/requests with an Idempotency-Key, the current base_revision, a reason, and a changes object. Only changed fields need to be included:
curl -X POST https://api.quadstate.us/messaging-policy/requests \
-H "Authorization: Bearer $QUAD_STATE_SMS_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: help-wording-change-1" \
-d '{"base_revision":1,"reason":"Update our customer service contact.","changes":{"help_message":"Example Company: For help call 270-555-1234. Reply STOP to opt out."}}'Allowed fields: opt_in_message, opt_out_message, help_message, opt_in_keywords, opt_out_keywords, and help_keywords. Messages must contain 20–255 characters and your account brand name. Keywords are case-insensitive alphanumeric strings, up to 20 characters each, supplied as arrays of up to 32 entries. Keyword actions cannot overlap. Existing standard opt-out keywords, START/UNSTOP, and HELP cannot be removed. Validation checks basic format; it does not constitute 10DLC approval.
A new request returns HTTP 202 with status pending. It contains its ID, original base revision, immutable previous and proposed policy snapshots, changes, reason, creation time, review note, and applied revision. Retry the same request and key to retrieve it without creating duplicates. Use GET /messaging-policy/requests to read your request box, optionally filtered by status=pending|approved|rejected|cancelled. Pagination uses limit (1–100) and before, with a next_cursor in the response. Retrieve one request with GET /messaging-policy/requests/{id}. Cancel a pending request with POST /messaging-policy/requests/{id}/cancel.
Requests do not immediately change the active policy. Quad State must review the wording and its compatibility with the registered campaign. Customer API tokens cannot approve requests or directly edit the active policy. Approval applies the full proposed policy atomically and advances its revision. Rejection leaves it unchanged and returns a review note. If another request was approved first, an older proposal cannot overwrite that newer revision; retrieve the current policy and submit a fresh request. Initial approval requires all three message texts.
Quad State content approval and 10DLC registration are separate. Approving wording does not enable SMS sending. Check number capabilities and activation status before sending. The console provides an active policy table, change-request form, and request box. Each account may have up to 20 pending requests and submit up to 10 new requests per minute.
Incoming STOP, START, and HELP messages
New incoming messages that match an active keyword contain a compliance object in message history and signed webhook events. Non-keyword messages, outbound messages, and older stored messages have null compliance metadata. The keyword's action, policy revision, and approved response are captured when the message is received:
{
"action": "opt_out",
"keyword": "STOP",
"policy_revision": 1,
"response_text": "Example Company: You have unsubscribed and will receive no further text messages.",
"automatically_sent": false
}Opt-out keywords immediately suppress ordinary sending to that recipient across the customer account. Opt-in keywords remove that suppression. HELP does not change suppression. Only approved extra keywords become active. response_text is null until approved wording exists. The API makes the approved response visible but does not automatically send it; ordinary outbound requests remain subject to suppression and activation checks. Keyword handling does not replace consent records or prove consent for marketing. Previously stored messages and queued signed event bodies retain their original content.
Signed webhooks
Configure a public HTTPS URL on port 443. Private network addresses, URL credentials, redirects, and callbacks to this API are rejected. Set the account url to null to pause numbers using that default; explicit number overrides remain active. Events remain queued.
curl -X PUT https://api.quadstate.us/webhook \
-H "Authorization: Bearer $QUAD_STATE_SMS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"url":"https://your-app.com/webhooks/quad-state-sms"}'Number webhook settings
All assigned numbers use the account default unless you change that number. Use your customer Bearer token with GET /numbers/{number}/webhook and PUT /numbers/{number}/webhook. Numbers belonging to another customer return 404.
PUT /numbers/+12705551234/webhook
{"url":"https://your-app.com/webhooks/number"}A non-null URL selects override. To restore the account default, send {"mode":"inherit"} or {"url":null}. To pause only this number, send {"mode":"disabled"}. Explicit override requires a URL; inherit and disabled accept only a null or omitted URL.
{
"number": "+12705551234",
"mode": "override",
"url": "https://your-app.com/webhooks/number",
"effective_url": "https://your-app.com/webhooks/number",
"signing_secret_scope": "account",
"events": ["sms.received", "sms.status_updated"]
}Incoming events use the destination number's settings. Outbound status events use the source number's settings. Every number URL uses the same account signing secret. The current settings apply when each attempt begins, including queued events; a request already in progress can still go to the previous URL. Paused events wait without consuming attempts. They can be delivered when you configure or resume their receiving URL.
Events: sms.received for incoming messages and sms.status_updated for outbound submission results. Each request carries X-Quad-State-Event-ID and X-Quad-State-Signature: t=TIMESTAMP,sha256=HEX_SIGNATURE.
{
"id": "evt_…",
"type": "sms.received",
"created_at": "2026-09-30T21:00:00Z",
"data": {
"id": "msg_…", "object": "sms_message", "direction": "inbound", "channel": "sms",
"from": "+16185551234", "to": "+12705551234", "text": "Thanks!",
"status": "received", "error_code": null,
"created_at": "2026-09-30T21:00:00Z", "updated_at": "2026-09-30T21:00:00Z"
}
}Verify HMAC-SHA256 over TIMESTAMP + "." + RAW_REQUEST_BODY using your signing secret. Verify the exact received bytes before parsing JSON; reject timestamps more than five minutes old or in the future, and compare signatures in constant time.
# Python receiver verification; raw_body must be bytes.
import hashlib, hmac, time
def verify(signature_header, raw_body, secret):
try:
parts = dict(item.split("=", 1) for item in signature_header.split(","))
stamp = parts["t"]
if abs(time.time() - int(stamp)) > 300:
return False
expected = hmac.new(secret.encode(), stamp.encode() + b"." + raw_body,
hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["sha256"])
except (ValueError, KeyError, TypeError):
return FalseSave and deduplicate events by id, then return any 2xx response. Delivery is at least once and ordering is not guaranteed. Attempts time out after 15 seconds; failures retry after 30 seconds, 60 seconds, and increasing delays capped at one hour, for a total of 12 attempts. Failed events remain available for manual retry. Each attempt has a new timestamp and signature but the same event ID and body. Pending events wait until a webhook URL is configured.
Your initial signing secret is issued during onboarding. If you need a replacement, call POST /webhook/rotate-secret; the response contains {"secret":"qs_whsec_…"}. The old secret stops being used immediately for the account default and every number override. Update your receiver promptly; an attempt already in progress may still use the previous secret.
Errors & limits
Activation and network errors
The Quad State API explains the failure category without exposing internal connection details. Number inventory, individual number details, and capabilities return an activation object when sending awaits activation. A blocked send returns HTTP 503 with the same information under error.details:
{
"error": {
"code": "sending_unavailable",
"message": "Quad State SMS sending is awaiting 10DLC registration approval and number activation.",
"request_id": "req_…",
"details": {
"reason": "activation_pending",
"activation_reason": "messaging_registration_pending",
"registration_status": "pending_verification",
"activation_status": "pending",
"send_status": "unavailable",
"receive_status": "available",
"action": "Contact Quad State to verify messaging registration and activate sending."
}
}
}pending_verification means Quad State has not verified registration approval; it is not a claim that registration was rejected. messaging_registration_pending identifies the 10DLC registration requirement, while number_activation_pending identifies number activation. Authentication remains valid when sending is awaiting activation.
| Reason | Meaning and action |
|---|---|
| activation_pending | Quad State has not enabled sending. Follow the activation explanation; no new message was submitted. |
| upstream_error | Quad State encountered an upstream messaging-network error. This can be a connectivity problem or a submission error; it does not establish an outage. Acceptance may be uncertain. |
| upstream_outage | Reserved for a confirmed upstream network outage. A single request failure is not classified as an outage. |
| submission_unknown | Quad State cannot confirm whether submission was accepted, such as after a timeout or unrecognized acknowledgment. Check the message before another send. |
| service_configuration_error | Quad State's messaging connection requires attention. Contact Quad State with the message ID; changing your customer token will not fix this. |
| message_rejected | Submission was rejected. Check the destination and message content, then contact Quad State if they are valid. |
For stored messages, error_code is accompanied by an error object containing its code, plain-language message, and next action. Both are null when there is no submission error. HTTP 202 means the API recorded a new submission; inspect its message status. For unknown, retrieve the record or replay the same request and Idempotency-Key. This retrieves the original result and does not resend. Starting a new send with a different key can duplicate a message. No automatic SMS retry is performed.
{"error":{"code":"sender_not_allowed","message":"The sending number is not enabled on your account.","request_id":"req_…"}}Errors use 400 for malformed requests, 401 for missing or revoked tokens, 403 for an unassigned sending number, 404 for unavailable resources, 409 for conflicting IDs, 413 for bodies over 16 KiB, 415 for an unsupported content type, 422 for validation, 429 for rate limits, and 503 when sending is unavailable. Include the X-Request-ID response header when contacting support.
Limits per customer: 300 API requests per minute, 60 new outbound messages per minute, and 30 manual webhook retries per minute. A 429 response includes Retry-After: 60. Network-level burst limits also apply. Authentication, sending availability, registration, and number provisioning must be completed by Quad State before sending.