SMS APIIntegration guide · v1https://sms.voctiv.com

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

  1. 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.
  2. Send a message with POST /api/v1/subaccounts/{subAccountId}/messages. The answer confirms it was accepted and gives its umid.
  3. 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

POST/api/v1/subaccounts/{subAccountId}/messages
{
  "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

POST/api/v1/subaccounts/{subAccountId}/messages/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

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"
}

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.

stateMeaning
queuedAccepted, waiting to be sent.
sentHanded to the mobile operator. Where the operator reports nothing further (the Philippines at present) this is the final status and delivered never follows.
deliveredDelivery confirmed. detail: delivered_to_recipient or delivered_to_operator.
undeliveredThe operator could not deliver. detail: undelivered_to_recipient or rejected_by_operator; errorCode and errorMessage say why.
rejectedRefused before sending; not billed.
unknownThe 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
  }
}
Receipts may arrive out of order and more than once: deduplicate by umid plus status.state. The first receipt (queued) also carries outboundContent with the text you sent. Webhook hosts must be public addresses.
errorCodeMeaning
7Delivery failure
8Deny (subscriber opted out or blacklisted)
15Invalid destination
19Invalid source address (sender ID not allowed)
33Route error (no route for this destination)
36SMS expired
46Subscriber not reachable
50Throttling error
51Time out error

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 11 alphanumeric or 16 numeric characters. Defaults to the sender ID of 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.
dlrCallbackUrlhttps 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" }
HTTPcodeMeaning
4001000 / 1001Invalid parameter / illegal subAccountId
4001002 / 3002Invalid / missing MSISDN
4001003Invalid encoding
4001004 / 3005Text too long / missing
4001005Invalid source
4001006 / 1011Invalid expiry / schedule time
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
429429Quota 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 withResult
01sent, then undelivered with errorCode 46 (subscriber not reachable).
02rejected at once with errorCode 19 (invalid source address).
03sent, no further status.
anything elsesent, 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.

The public sandbox is shared: anyone with the key can read its messages, so never send real numbers or texts there. Limits: 20 messages per minute from one address, 500 per hour for all users together, 10 messages per batch. Webhook setup and log exports are not available, callbacks must be public addresses, and messages are deleted after 24 hours. For your own webhook, history and higher limits, ask us for a personal test key.