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
- Send one SMS
- Send a batch
- Message status
- Delivery receipts
- Cancel, batches, feedback, PII
- Log export
- Prices and webhooks
- Idempotency
- Fields and limits
- Errors
- Test environment
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
| state | Meaning |
|---|---|
queued | Accepted by the gateway, waiting for a provider. |
sent | Handed to the mobile operator, no final receipt yet. |
delivered | Delivery confirmed. detail: delivered_to_recipient or delivered_to_operator. |
undelivered | Operator reported a failure. detail: undelivered_to_recipient or rejected_by_operator; errorCode and errorMessage explain why. |
rejected | Refused by the gateway or by every route; not billed. |
unknown | The 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
| errorCode | Meaning |
|---|---|
| 7 | Delivery failure |
| 8 | Deny (subscriber opted out or blacklisted) |
| 15 | Invalid destination |
| 17 | Invalid message length |
| 19 | Invalid source address (sender ID not allowed) |
| 33 | Route error (no route for this destination) |
| 36 | SMS expired |
| 42 | Source credit insufficiency |
| 46 | Subscriber not reachable |
| 50 | Throttling error |
| 51 | Time 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
| Field | Rules |
|---|---|
destination | E.164, with or without + (+639171234567). National format is accepted together with country (ISO 3166-1 alpha-2). |
text | Required. Up to 10 segments: 160 GSM-7 characters for one segment, 153 per segment when concatenated; 70 and 67 for Unicode. |
encoding | AUTO (default), GSM7 or UCS2. The response returns the encoding actually used. |
source | Sender 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. |
clientMessageId | Your reference, up to 350 characters; returned in responses and receipts. |
clientBatchId | Your batch reference, up to 20 characters. |
scheduled | RFC 3339 time to send the message at; must not be in the past. |
expiry | RFC 3339 time after which the message is not sent. |
dlrCallbackUrl | http(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" }
| HTTP | code | Meaning |
|---|---|---|
| 400 | 1000 / 1001 | Invalid parameter / illegal subAccountId |
| 400 | 1002 / 3002 | Invalid / missing MSISDN |
| 400 | 1003 | Invalid encoding |
| 400 | 1004 / 3005 | Text too long / missing |
| 400 | 1005 | Invalid source |
| 400 | 1006 / 1011 | Invalid expiry / schedule time |
| 400 | 1009 | Invalid country code |
| 400 | 3009 / 3014 | clientMessageId / clientBatchId too long |
| 400 | 3012 / 1100 | No messages / more than 10 000 messages |
| 400 | 3020 | Invalid dlrCallbackUrl |
| 401 | 1200 | Missing, wrong or revoked API key |
| 403 | 1201 | Subaccount not yours, disabled, or source IP not allowed |
| 404 | 1300 | Message not found |
| 409 | 1000 | Idempotency-Key reused for a different request |
| 429 | 429 | Quota 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 with | What happens |
|---|---|
| 00 (or any other) | accepted, then delivered |
| 01 | sender ID refused, every fallback refused, final rejected with errorCode 19 |
| 02 | provider out of credit, rejected with errorCode 42 |
| 03 | provider hangs, final unknown with errorCode 51 |
| 04 | accepted, then undelivered with errorCode 46 |
| 05 | accepted, no delivery receipt ever (stays sent) |
| 06 | accepted, then sent, then delivered |