Quick start
- Buy SMS credits and get a sender ID approved. Apply on Sender IDs in your dashboard.
- Create an API key on API keys. It is shown once, so store it in your server's environment (never in a browser or mobile app).
- Send your first message:
curl https://www.ceytrexlabs.com/api/v1/sms/send \
-H "Authorization: Bearer $CEYTREX_SMS_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-10293" \
-d '{
"sender_id": "ACME",
"to": ["0771234567", "+94712345678"],
"body": "ACME: Your order #10293 has shipped.",
"type": "transactional"
}'Prefer clicking first? Send a test SMS from the dashboard. It goes through the same pipeline as the API.
Authentication
Every request carries your key in the Authorization header. Keys look like ceysk_live_ followed by 40 letters and digits.
Authorization: Bearer ceysk_live_3kQ9...- We store only a one-way HMAC-SHA256 hash, so a lost key can't be recovered. Revoke it and create another.
- Use one key per system. Up to 10 active keys per account; each shows when it was last used.
- A missing, wrong or revoked key answers
401 unauthenticated.
Sender IDs
Sri Lankan networks block SMS from unregistered sender names, so every send names one of your approved sender IDs in sender_id, exactly as approved (case matters). Anything else answers 403 sender_id_not_approved.
Sender IDs are 3 to 11 characters, contain at least one letter, and must match your business or brand. Apply with your business registration and an authorisation letter on /dashboard/sms/sender-ids. Use separate sender IDs for transactional and promotional traffic where you can.
Send a message
POST/api/v1/sms/send
Sends the same body to 1 to 1,000 numbers. Credits for every recipient are taken before anything goes out, and refunded automatically for any message the network refuses or fails to deliver.
| Field | Type | Notes |
|---|---|---|
sender_id | string | Required. One of your active sender IDs. |
to | string or array | Required. Sri Lankan mobiles: 0771234567, 771234567, 94771234567 or +94771234567. Spaces and dashes are fine. A string may hold comma-separated numbers. Duplicates are removed; landlines and invalid numbers are listed in recipients.invalid. |
body | string | Required. Up to 6 SMS parts (918 GSM-7 or 402 Unicode characters). |
type | string | Required. "transactional" (OTPs, alerts, receipts) or "promotional" (offers, campaigns). See promotional rules. |
client_reference | string | Optional. Your id for this send, used for idempotency when no Idempotency-Key header is sent. |
Response (200):
{
"id": "6f1c2a0e-9b1d-4c55-8a8e-2b7d1f0c9a11",
"object": "sms.batch",
"sender_id": "ACME",
"type": "transactional",
"encoding": "GSM-7",
"segments_per_message": 1,
"recipients": { "accepted": 2, "invalid": [], "duplicates_removed": 0 },
"credits_charged": 2,
"credits_refunded": 0,
"balance_after": 4818,
"levy": { "applies": false },
"messages": [
{ "id": "0b8e...e1", "to": "+94771234567", "status": "sent", "error": null },
{ "id": "4c1a...7f", "to": "+94712345678", "status": "sent", "error": null }
],
"created_at": "2026-10-19T09:32:11.000Z"
}credits_charged is what the send cost after refunds, and balance_after is your wallet afterwards. Scheduled sends are not available yet; a scheduled_at field is refused rather than ignored.
Segments & Unicode
One credit pays for one SMS part to one number. Long messages are split into parts, and each part carries a small header, so the per-part limit drops once a message is split.
| Encoding | When | 1 part | Each part when split |
|---|---|---|---|
| GSM-7 | Plain English letters, digits, common symbols | 160 chars | 153 chars |
| UCS-2 (Unicode) | Any Sinhala, Tamil, emoji or other non-GSM character | 70 chars | 67 chars |
- One Sinhala or Tamil letter switches the whole message to Unicode. A 100-character English message with one Sinhala word becomes 2 parts.
- In GSM-7, the characters
^ { } [ ] ~ | € \count as two. - Emoji outside the basic plane (most of them) count as two Unicode characters.
- Every response tells you the
encodingandsegments_per_message, and the dashboard test form counts parts as you type.
Promotional rules
Sri Lanka treats marketing SMS differently from service messages (TRCSL guideline, Gazette 2104/16). Mark offers, discounts and campaigns as "type": "promotional". The API then enforces:
- Hours: 08:00 to 20:00 Sri Lanka time only. Outside that window the send answers
422 quiet_hourswithnext_window_atand aRetry-Afterheader. - Opt-out: every promotional message must say how to stop, for example "Opt out: reply STOP" or an unsubscribe link. Without one it answers
400 opt_out_missing. - Levy: the government levy of LKR 0.25 per promotional SMS is included in our per-SMS price, so promotional parts cost the same one credit. The response's
levyobject shows this. - Send only to people who agreed to hear from you, and keep a record of that consent.
Transactional messages (OTPs, delivery updates, bills) are not limited by these rules.
Balance
GET/api/v1/sms/balance
{
"object": "sms.balance",
"credits": 4818,
"currency": "LKR",
"rate_per_credit": "0.85",
"value": "4095.30"
}Top up from Enterprise SMS pricing. Credits are added as soon as the payment clears.
Messages & status
GET/api/v1/sms/messages/{id}
GET/api/v1/sms/messages?since=&status=&before=&limit=
Look up one message, or list your messages newest first. since and before take ISO 8601 times, status filters, and limit is 1 to 100 (default 50). When has_more is true, pass next_before as before to get the next page.
| Status | Meaning |
|---|---|
queued | Accepted and charged, being handed to the network. |
sent | The network accepted it. Waiting for the handset. |
delivered | The handset received it. Final. |
failed | Invalid number, unreachable handset or network failure. Credits refunded. Final. |
rejected | Refused before delivery, for example the recipient blocked your sender ID. Credits refunded. Final. |
Delivery reports
The mobile networks report delivery back to us through a signed delivery-report webhook, and we update each message's status. Reports can arrive out of order, so a final status (delivered, failed, rejected) is never overwritten by a later "sent". A message that fails delivery is refunded once, however many reports arrive.
To follow delivery from your side, poll GET /api/v1/sms/messages?since= with the time of your last poll, or look up the ids you got back from the send. Push webhooks to your own URL are planned; ask us if you need them early.
Idempotency
Networks drop connections. To retry safely, send an Idempotency-Key header (or a client_reference field) of up to 100 visible characters, such as your order id.
- Same key, same body: you get the original result back (with
idempotent_replay: true), and nothing is sent or charged twice. - Same key, different body:
409 idempotency_conflict. - If the whole send failed upstream (
502 upstream_unavailable), the key is released, so retrying with it sends for real.
Rate limits
Each key may send up to 1,000 messages (recipients) in any rolling minute. Over the limit, the send answers 429 rate_limited with Retry-After. Successful sends return RateLimit-Limit and RateLimit-Remaining headers. Need more? Talk to us about a higher limit.
Errors
Errors always use the same JSON envelope and a stable code you can branch on. Quote the request_id when you contact support.
{
"error": {
"code": "insufficient_credits",
"message": "Not enough SMS credits for this send. Top up and try again.",
"credits_required": 120,
"balance": 40,
"request_id": "req_k2V9xQ1bT0aZ",
"doc_url": "https://www.ceytrexlabs.com/developers/sms#error-insufficient_credits"
}
}| HTTP | code | When |
|---|---|---|
| 400 | invalid_request | Malformed JSON, a missing field or a bad value. The message says which. |
| 400 | no_valid_recipients | Nothing left after removing invalid numbers (the list is in `invalid`). |
| 400 | too_many_recipients | More than 1,000 numbers in one request. |
| 400 | message_too_long | Body over 6 SMS parts. |
| 400 | opt_out_missing | A promotional message without an opt-out line. |
| 401 | unauthenticated | Missing, malformed or revoked API key. |
| 402 | insufficient_credits | Your wallet can't cover every recipient. Nothing was sent or charged. |
| 403 | sender_id_not_approved | The sender ID isn't approved and active on your account. |
| 403 | forbidden | The resource belongs to another account. |
| 404 | not_found | No message with that id on your account. |
| 409 | idempotency_conflict | The same key was used before with a different body. |
| 422 | quiet_hours | A promotional send between 20:00 and 08:00 Sri Lanka time. See `next_window_at`. |
| 429 | rate_limited | Your key sent too many messages in the last minute. Honour Retry-After. |
| 500 | internal_error | Our fault. Retry with the same Idempotency-Key. |
| 502 | upstream_unavailable | The mobile network refused or timed out. Credits are refunded; retry with the same key. |
| 503 | provider_not_configured | Sending isn't switched on yet for this server. |
| 503 | not_ready | The API is being set up. Try again later. |
Ready to send? It takes a few minutes.
Create a key, send a test to your own phone, then point your system at the API.
Last updated 2026-10-18. API version v1.