Developers · eSMS API v1

Send SMS from your code, under your own brand.

One REST endpoint for OTPs, order updates and campaigns to every Sri Lankan mobile network. JSON in, JSON out, prepaid credits at LKR 0.85 per SMS part, and clear Sinhala and Tamil Unicode rules.

Base URL
/api/v1/sms
Auth
Bearer key
Per request
1,000 numbers
Format
JSON, UTF-8

Quick start

  1. Buy SMS credits and get a sender ID approved. Apply on Sender IDs in your dashboard.
  2. 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).
  3. 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.

FieldTypeNotes
sender_idstringRequired. One of your active sender IDs.
tostring or arrayRequired. 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.
bodystringRequired. Up to 6 SMS parts (918 GSM-7 or 402 Unicode characters).
typestringRequired. "transactional" (OTPs, alerts, receipts) or "promotional" (offers, campaigns). See promotional rules.
client_referencestringOptional. 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.

EncodingWhen1 partEach part when split
GSM-7Plain English letters, digits, common symbols160 chars153 chars
UCS-2 (Unicode)Any Sinhala, Tamil, emoji or other non-GSM character70 chars67 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 encoding and segments_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_hours with next_window_at and a Retry-After header.
  • 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 levy object 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.

StatusMeaning
queuedAccepted and charged, being handed to the network.
sentThe network accepted it. Waiting for the handset.
deliveredThe handset received it. Final.
failedInvalid number, unreachable handset or network failure. Credits refunded. Final.
rejectedRefused 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"
  }
}
HTTPcodeWhen
400invalid_requestMalformed JSON, a missing field or a bad value. The message says which.
400no_valid_recipientsNothing left after removing invalid numbers (the list is in `invalid`).
400too_many_recipientsMore than 1,000 numbers in one request.
400message_too_longBody over 6 SMS parts.
400opt_out_missingA promotional message without an opt-out line.
401unauthenticatedMissing, malformed or revoked API key.
402insufficient_creditsYour wallet can't cover every recipient. Nothing was sent or charged.
403sender_id_not_approvedThe sender ID isn't approved and active on your account.
403forbiddenThe resource belongs to another account.
404not_foundNo message with that id on your account.
409idempotency_conflictThe same key was used before with a different body.
422quiet_hoursA promotional send between 20:00 and 08:00 Sri Lanka time. See `next_window_at`.
429rate_limitedYour key sent too many messages in the last minute. Honour Retry-After.
500internal_errorOur fault. Retry with the same Idempotency-Key.
502upstream_unavailableThe mobile network refused or timed out. Credits are refunded; retry with the same key.
503provider_not_configuredSending isn't switched on yet for this server.
503not_readyThe 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.

Call Us
Get in Touch