Quad State SMS API

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 its consent records, HELP handling, and opt-out responses.

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.

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.

MethodEndpointPurpose
GET/accountYour account ID and name
GET/numbersYour assigned numbers and channel capabilities
GET/numbers/{number}One assigned number and its capabilities
GET/numbers/{number}/capabilitiesSMS, MMS, RCS, and iMessage availability
POST/messagesSubmit a message with an optional channel
GET/messagesMessage history; limit=1–100, before=message ID
GET/messages/{id}Retrieve a message
GET / PUT/webhookRead or set the receiving URL
POST/webhook/rotate-secretReplace your signing secret
GET/webhook/deliveriesLatest 100 webhook deliveries
POST/webhook/deliveries/{id}/retryReplay an event
GET/healthService 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.

Signed webhooks

Configure a public HTTPS URL on port 443. Private network addresses, URL credentials, redirects, and callbacks to this API are rejected. Set url to null to pause deliveries; 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"}'

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 False

Save 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. Update your receiver promptly; an attempt already in progress may still use the previous secret.

Errors & limits

{"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.