Send SMS through the API
A JSON REST API to send text messages and learn what happened to them. Every request goes to https://sms.voctiv.com over HTTPS; all times are UTC.
Getting started
- You receive an API key and a subaccount id. The key authenticates you; the subaccount id is part of every path and carries your settings: default sender ID, webhook, pricing.
- Send a message with
POST /api/v1/subaccounts/{subAccountId}/messages. The answer confirms it was accepted and gives itsumid. - Get the outcome either from delivery receipts posted to your webhook, or by
GET …/messages/{umid}.
Authentication
Put the API key in the Authorization header of every request. Keys can be restricted to your source IP addresses on request.
Authorization: Bearer <api key>
Content-Type: application/json
Send a message
{
"destination": "+639171234567",
"text": "Your code is 4921",
"source": "ACME",
"clientMessageId": "order-1001"
}
{
"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 message is accepted; delivery is reported later. Only destination and text are required; source defaults to the sender ID configured for your subaccount. A request that fails validation gets an HTTP 400 with an error object.
If a network error leaves you unsure whether the request arrived, repeat it with the same Idempotency-Key: <your unique value> header: you get the original answer back and no second message is created. A repeat that arrives while the first request is still being processed gets 409 with Retry-After: 1: wait and repeat it again.
During maintenance (a database restore) new messages are refused with 503 and a Retry-After header; nothing is accepted, so repeat the request later.
Send a batch
Up to 10 000 messages in one request. Use messages[] for personalised texts, or destinations[] with a template for the same text to many numbers. Fields of a message override the template.
{
"clientBatchId": "promo-2026-10",
"messages": [
{ "destination": "+639171234500", "clientMessageId": "a-1" },
{ "destination": "+639171234506", "clientMessageId": "a-2", "text": "Personal text" }
],
"template": { "source": "ACME", "text": "Shared text" },
"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" } }, … ]
}
A customer may have an hourly limit: at most N messages in any 60 minutes across its subaccounts. A request that would exceed it is refused as a whole with HTTP 429, code 429 and a Retry-After header saying in how many seconds room frees up; a batch is never split. Nothing from such a request is sent or billed.
Messages that fail validation come back with status.code = "REJECTED" and a reason in description; they are not sent and not billed.
Message status
{
"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"
}
The number and the text are masked by default; unmasking can be enabled for your subaccount. price appears once the message is accepted for delivery and a tariff applies.
| state | Meaning |
|---|---|
queued | Accepted, waiting to be sent. |
sent | Handed to the mobile operator. Where the operator reports nothing further (the Philippines at present) this is the final status and delivered never follows. |
delivered | Delivery confirmed. detail: delivered_to_recipient or delivered_to_operator. |
undelivered | The operator could not deliver. detail: undelivered_to_recipient or rejected_by_operator; errorCode and errorMessage say why. |
rejected | Refused before sending; not billed. |
unknown | The outcome could not be established, for example a timeout after submission. Not retried automatically, or no delivery report arrived within the time the route allows. |
Delivery receipts
On every status change a JSON receipt is POSTed 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. Answer with any 2xx status; failed calls (timeout, 4xx, 5xx) are retried after 1, 10, 30 and 90 seconds.
{
"namespace": "SMS",
"eventType": "outbound_message_status_changed",
"description": "SMS outbound message delivery receipt",
"payload": {
"umid": "01a17e34-0c6d-77e9-ac07-8f8eb750c8d3",
"clientMessageId": "order-1001",
"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
}
}
umid plus status.state. The first receipt (queued) also carries outboundContent with the text you sent. Webhook hosts must be public addresses.| errorCode | Meaning |
|---|---|
| 7 | Delivery failure |
| 8 | Deny (subscriber opted out or blacklisted) |
| 15 | Invalid destination |
| 19 | Invalid source address (sender ID not allowed) |
| 33 | Route error (no route for this destination) |
| 36 | SMS expired |
| 46 | Subscriber not reachable |
| 50 | Throttling error |
| 51 | Time out error |
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 11 alphanumeric or 16 numeric characters. Defaults to the sender ID of 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 | https URL that overrides the subaccount webhook for this message. |
Requests are rate-limited 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. The errorId also comes back in the X-Request-Id header of every response; quote it when you contact us.
{ "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 | 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 |
| 429 | 429 | Quota exceeded, see Retry-After |
Testing
Ask for a test key (it starts with sgk_test_) to build and check your integration without sending real SMS. A test key works with the same subaccount, paths, fields, validation and limits as a live one, and your webhook gets the same receipts; but no message leaves the gateway and nothing is billed. Switch to the live key when you are ready, nothing else changes.
With a test key, the last two digits of the destination number choose what happens. A status follows about two seconds after acceptance.
| Number ends with | Result |
|---|---|
01 | sent, then undelivered with errorCode 46 (subscriber not reachable). |
02 | rejected at once with errorCode 19 (invalid source address). |
03 | sent, no further status. |
| anything else | sent, then delivered. |
Public sandbox: try it now
To try the API before you have your own keys, use the public sandbox. Anyone can use it, it sends nothing and costs nothing.
API key: sgk_test_public_sandbox
subAccountId: sandbox
curl -X POST https://sms.voctiv.com/api/v1/subaccounts/sandbox/messages \
-H 'Authorization: Bearer sgk_test_public_sandbox' \
-H 'Content-Type: application/json' \
-d '{"destination": "+639171234500", "text": "Hello"}'
Then GET /api/v1/subaccounts/sandbox/messages/{umid} shows the status. To get receipts, pass your own HTTPS address in dlrCallbackUrl of each message.