SMS GatewayIntegration guide for partners · API v1

Send SMS through the gateway

The API follows the request and response shapes of the 8x8 Connect SMS API: if your application already talks to 8x8, point it at this host and use the API key you received from us. Base URL: https://sms.voctiv.com.

Authentication

Every request carries your API key as a bearer token. Keys are issued per partner and can be restricted to a list of source IP addresses on request.

Authorization: Bearer <api key>
Content-Type: application/json

Your subAccountId is part of every path. It identifies the sending context we configured for you: default sender ID, webhook URL and, later, pricing.

Send one SMS

POST /api/v1/subaccounts/{subAccountId}/messages

{
  "destination": "+639171234567",
  "source": "ACME",
  "text": "Your code is 4921",
  "encoding": "AUTO",
  "clientMessageId": "order-1001"
}

Response 200 OK:

{
  "umid": "01a17e34-0c6d-77e9-ac07-8f8eb750c8d3",
  "clientMessageId": "order-1001",
  "destination": "639171234567",
  "encoding": "GSM7",
  "status": { "code": "QUEUED", "description": "SMS is accepted and queued for processing" }
}
QUEUED means the gateway accepted the message. Delivery is reported later through delivery receipts or GET message status.

Send a batch

Up to 10 000 messages per request. Use messages[] for personalised content, or destinations[] with a template for the same text to many numbers. Fields of a message override the template.

POST /api/v1/subaccounts/{subAccountId}/messages/batch

{
  "clientBatchId": "promo-2026-10",
  "messages": [
    { "destination": "+639171234500", "clientMessageId": "a-1" },
    { "destination": "0917 123 4506", "country": "PH", "clientMessageId": "a-2", "text": "Personal text" }
  ],
  "template": { "source": "ACME", "text": "Shared text", "encoding": "AUTO" },
  "includeMessagesInResponse": true
}
{
  "batchId": "01a17df5-11fa-73db-bb58-e3b8266a684d",
  "clientBatchId": "promo-2026-10",
  "acceptedCount": 2,
  "rejectedCount": 0,
  "messages": [ { "umid": "...", "clientMessageId": "a-1", "destination": "639171234500", "encoding": "GSM7",
                  "status": { "code": "QUEUED", "description": "SMS is accepted and queued for processing" } }, ... ]
}

Messages that fail validation come back with status.code = "REJECTED" and one of: Text value is missing, Invalid Text value. Reached max allowed limit, Invalid Source value. Reached max allowed limit, Expiry time already reached, Invalid MSISDN format (not E.164 international number). They are not sent and not billed.

Message status

GET /api/v1/subaccounts/{subAccountId}/messages/{umid}

{
  "umid": "01a17e34-0c6d-77e9-ac07-8f8eb750c8d3",
  "subAccountId": "acme-main",
  "direction": "outbound",
  "country": "PH",
  "status": { "state": "delivered", "detail": "delivered_to_recipient", "timestamp": "2026-10-27T16:31:42.71Z" },
  "msisdn": "+63917123xxxx",
  "source": "ACME",
  "body": "**MASKED**",
  "encoding": "GSM7",
  "createdAt": "2026-10-27T16:31:42.70Z",
  "smsCount": 1,
  "price": { "total": 0.02, "perSms": 0.02, "currency": "USD" },
  "clientMessageId": "order-1001"
}

Phone number and text are masked by default; unmasking can be enabled for your subaccount. price appears once a provider accepted the message and a tariff applies to your subaccount; rejected messages carry no price.

States

stateMeaning
queuedAccepted by the gateway, waiting for a provider.
sentHanded to the mobile operator, no final receipt yet.
deliveredDelivery confirmed. detail: delivered_to_recipient or delivered_to_operator.
undeliveredOperator reported a failure. detail: undelivered_to_recipient or rejected_by_operator; errorCode and errorMessage explain why.
rejectedRefused by the gateway or by every route; not billed.
unknownThe outcome could not be established (for example a timeout after submission). Not retried automatically.

Delivery receipts

On every status change we POST a JSON receipt to the webhook URL configured for your subaccount, or to the dlrCallbackUrl given in the message. A static Authorization header can be configured so that you can verify the sender. Failed calls (timeout, 4xx, 5xx) are retried after 1, 10, 30 and 90 seconds. Answer with any 2xx status.

{
  "namespace": "SMS",
  "eventType": "outbound_message_status_changed",
  "description": "SMS outbound message delivery receipt",
  "payload": {
    "umid": "01a17e34-0c6d-77e9-ac07-8f8eb750c8d3",
    "batchId": "01a17df5-11fa-73db-bb58-e3b8266a684d",
    "clientMessageId": "order-1001",
    "clientBatchId": "promo-2026-10",
    "subAccountId": "acme-main",
    "source": "ACME",
    "destination": "+639171234567",
    "status": { "state": "undelivered", "detail": "undelivered_to_recipient", "timestamp": "2026-10-27T16:31:45.12Z",
                "errorCode": 46, "errorMessage": "Handset not reachable" },
    "smsCount": 1
  }
}

The first receipt (queued) also carries outboundContent with the text you sent. Receipts may arrive out of order and more than once: deduplicate by umid plus status.state.

Common error codes in receipts

errorCodeMeaning
7Delivery failure
8Deny (subscriber opted out or blacklisted)
15Invalid destination
17Invalid message length
19Invalid source address (sender ID not allowed)
33Route error (no route for this destination)
36SMS expired
42Source credit insufficiency
46Subscriber not reachable
50Throttling error
51Time out error

Cancel, batches, feedback, PII

DELETE /api/v1/subaccounts/{subAccountId}/messages/{umid}             → { "cancelledCount": 1 }
DELETE /api/v1/subaccounts/{subAccountId}/messages/batch/{batchId}     → { "cancelledCount": 12 }
GET    /api/v1/subaccounts/{subAccountId}/batches/{batchId}            → batch summary with counts by state
POST   /api/v1/subaccounts/{subAccountId}/messages/{umid}/feedback     { "outcome": "success" | "failure", "timestamp": "..." }
DELETE /api/v1/subaccounts/{subAccountId}/messages/{umid}/pii          → 202, text and number are erased for good

Only messages that were scheduled for later and not yet submitted can be cancelled; a cancelled message ends in state rejected with errorCode 20 (Local cancel).

Log export

POST /api/v1/subaccounts/{subAccountId}/messages/exports
{ "from": "2026-10-01", "to": "2026-11-01", "phoneNumber": "+639171234567", "countries": ["PH"], "limit": 0 }
→ 202 { "jobId": "..." }

GET /api/v1/subaccounts/{subAccountId}/messages/exports/{jobId}
→ { "status": "succeeded", "data": ["https://.../messages/download/{token}"], "expires": "2026-11-27T..." }

Statuses: queued, running, succeeded, empty, failed, cancelled (DELETE on the job). The CSV has the 8x8 columns: umid, subaccountid, direction, country, operator, mcc, mnc, status_value, status_details, status_errorcode, status_errormessage, status_timestamp, msisdn, source, encoding, createdat, smscount, price_currency, price_persms, price_total, clientmessageid, clientbatchid, batchid, body. Dates default to the last seven days; links stay valid for 30 days.

Prices and webhooks

Account-level endpoints take your accountId (we give it to you with the API key):

GET  /api/v1/accounts/{accountId}/prices?country=PH          price per SMS segment per subaccount and country
GET  /api/v1/accounts/{accountId}/balance                    403 — accounts are postpaid
GET  /api/v1/accounts/{accountId}/webhooks                   configured receipt webhooks
POST /api/v1/accounts/{accountId}/webhooks
[ { "subAccountId": "*", "url": "https://partner.example/dlr", "httpAuthorization": "Bearer ...", "enabled": true } ]
DELETE /api/v1/accounts/{accountId}/webhooks   [ { "subAccountId": "*" } ]

subAccountId: "*" applies to all your subaccounts. OAuth client-credentials webhooks are not supported; use a static httpAuthorization value. Webhook hosts must be public addresses.

Idempotency

Send Idempotency-Key: <your unique value> with a single or batch request to make network retries safe: a repeated request with the same key returns the original answer (header Idempotent-Replayed: true) and creates no new messages. Keys live for 24 hours; the same key on a different endpoint is refused with 409.

Fields and limits

FieldRules
destinationE.164, with or without + (+639171234567). National format is accepted together with country (ISO 3166-1 alpha-2).
textRequired. Up to 10 segments: 160 GSM-7 characters for one segment, 153 per segment when concatenated; 70 and 67 for Unicode.
encodingAUTO (default), GSM7 or UCS2. The response returns the encoding actually used.
sourceSender ID, up to 16 characters (11 for alphanumeric). Defaults to the sender ID configured for your subaccount. Sender IDs must be registered with the operators in advance.
clientMessageIdYour reference, up to 350 characters; returned in responses and receipts.
clientBatchIdYour batch reference, up to 20 characters.
scheduledRFC 3339 time to send the message at; must not be in the past.
expiryRFC 3339 time after which the message is not sent.
dlrCallbackUrlhttp(s) URL that overrides the subaccount webhook for this message.

Rate limits apply per subaccount and per source IP. When exceeded the API answers 429 with a Retry-After header (seconds).

Errors

Every error response has the same body:

{ "code": 1002, "message": "Invalid MSISDN format (not E.164 international number)",
  "errorId": "2fb918d1-c255-45df-8796-23e6d9e8daa8", "timestamp": "2026-10-27T15:22:08.75Z" }
HTTPcodeMeaning
4001000 / 1001Invalid parameter / illegal subAccountId
4001002 / 3002Invalid / missing MSISDN
4001003Invalid encoding
4001004 / 3005Text too long / missing
4001005Invalid source
4001006 / 1011Invalid expiry / schedule time
4001009Invalid country code
4003009 / 3014clientMessageId / clientBatchId too long
4003012 / 1100No messages / more than 10 000 messages
4003020Invalid dlrCallbackUrl
4011200Missing, wrong or revoked API key
4031201Subaccount not yours, disabled, or source IP not allowed
4041300Message not found
4091000Idempotency-Key reused for a different request
429429Quota exceeded, see Retry-After

Every response carries X-Request-Id; the same value is the errorId of an error body. Quote it when you contact us.

Test environment

On the test stand messages are not delivered to real phones. The outcome is chosen by the last two digits of the destination number, so you can exercise every status your integration must handle:

Ends withWhat happens
00 (or any other)accepted, then delivered
01sender ID refused, every fallback refused, final rejected with errorCode 19
02provider out of credit, rejected with errorCode 42
03provider hangs, final unknown with errorCode 51
04accepted, then undelivered with errorCode 46
05accepted, no delivery receipt ever (stays sent)
06accepted, then sent, then delivered