SMS API
The SMS API sends SMS messages, reports their delivery, sends and verifies one-time codes, and notifies your system by webhook when a message changes status. Messages are prepaid: each one is charged to your wallet when it is accepted.
[!TIP]
Try it here: enter your Client ID and Client Secret in the Authentication box, click Authorize, then use Test Request on any endpoint.
What you can do
| Capability |
Endpoint |
Description |
| Verify context |
GET /me |
Check your token and organization |
| Send a message |
POST /messages |
Send one SMS |
| Send to many |
POST /messages/bulk |
Send one text to up to 10,000 numbers |
| Message status |
GET /messages/{id} |
Read one message and its delivery status |
| List messages |
GET /messages |
Search your messages, with filters and paging |
| List batches |
GET /batches |
Your bulk requests, newest first |
| Batch progress |
GET /batches/{id} |
Per-status counts of a bulk request |
| Send a code |
POST /otp/send |
Send a one-time code by SMS |
| Verify a code |
POST /otp/verify |
Check the code the user typed |
| Sender IDs |
GET/POST /sender-ids |
List your sender IDs and request new ones |
| Webhooks |
Console → Webhooks |
Receive message status, batch, sender ID, balance and OTP events at your own URL — see Webhooks |
| Wallet |
GET /wallet |
Balance and account mode (live or test) |
| Prices |
GET /pricing |
Your price per segment by destination prefix |
| Statistics |
GET /stats |
Totals and per-day traffic for the last 1 to 90 days |
| Health |
GET /health |
Service availability (no token) |
Quick start
1. Get a token. Take your client_id and client_secret from OmaxTelecom Console. More in Getting started.
TOKEN=$(curl -s -X POST https://api.omaxtelecom.com/auth/token \
-d grant_type=client_credentials \
-d client_id="$CLIENT_ID" \
-d client_secret="$CLIENT_SECRET" | jq -r .access_token)
2. Check that it works.
curl -s https://api.omaxtelecom.com/sms/me -H "Authorization: Bearer $TOKEN"
data.organization.sms_enabled must be true. If it is false, SMS has not been set up for your organization yet — contact support.
3. Send a message.
curl -s -X POST https://api.omaxtelecom.com/sms/messages \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1042-confirmation" \
-d '{
"from": "MYSHOP",
"to": "+38765123456",
"text": "Your order has shipped.",
"reference": "order-1042"
}'
{
"success": true,
"data": {
"id": "0198c5f2-7a10-7c3e-9d4b-2f6a1e8b0c11",
"batch_id": null,
"source": "api",
"from": "MYSHOP",
"to": "38765123456",
"status": "queued",
"error_code": null,
"segments": 1,
"price": { "amount": "0.055", "currency": "EUR" },
"reference": "order-1042",
"is_test": false,
"is_otp": false,
"created_at": "2026-10-12T12:00:00+00:00",
"delivered_at": null,
"updated_at": "2026-10-12T12:00:00+00:00",
"text": "Your order has shipped."
}
}
4. Follow the delivery. Read GET /messages/{id} until the status is final, or subscribe to webhooks and be told.
With the Node.js SDK
The Node.js SDK gets the token, sends the Idempotency-Key and retries safely for you:
import { Omax } from "@omaxtelecom/sdk";
const omax = new Omax(); // reads OMAX_CLIENT_ID and OMAX_CLIENT_SECRET
const message = await omax.sms.messages.send({
from: "ACME",
to: "+38765123456",
text: "Your order has shipped.",
reference: "order-1042",
});
const current = await omax.sms.messages.get(message.id);
for await (const m of await omax.sms.messages.list({ status: "delivered", reference: "order-1042" })) {
console.log(m.id, m.to);
}
Errors are thrown as OmaxApiError with the same code, status and requestId as described in Responses.
Authentication
Send an access token on every request except GET /health:
Authorization: Bearer <access_token>
Content-Type: application/json
Accept: application/json
Get the token from POST https://api.omaxtelecom.com/auth/token with your Console client_id and client_secret. Getting started shows how, with code samples.
The token identifies your organization; there is nothing else to send. Keep the token in memory, reuse it for every request, and get a new one shortly before it expires or when a request answers 401.
Responses
Successful responses:
{
"success": true,
"data": { ... }
}
Lists add meta with page, per_page, total and last_page.
Error responses:
{
"success": false,
"error": {
"code": "INVALID_SENDER",
"message": "The sender ID is not approved for this organization.",
"request_id": "9f1c2d3e-4b5a-4c6d-8e7f-0a1b2c3d4e5f",
"details": { "message_id": "0198c5f2-7a10-7c3e-9d4b-2f6a1e8b0c11", "smpp_status": "RINVSRCADR" }
}
}
Branch on error.code and the HTTP status, never on message. Validation errors add fields with the messages per request field; rejected sends add details. Quote request_id (also in the X-Request-Id response header) when you contact support.
Sending a message
POST /messages with an Idempotency-Key header.
| Field |
Required |
Rules |
from |
yes |
An approved sender ID: up to 11 letters and digits (MYSHOP), or a number of 3 to 15 digits |
to |
yes |
International number, with or without +. Spaces, dashes and brackets are ignored |
text |
yes |
Up to 1600 characters and at most 6 segments |
reference |
no |
Your own identifier, up to 128 characters. You can filter the message list by it |
Message length and segments
A long text is split into segments, and every segment is charged.
| Text contains |
One message |
Each segment when split |
| Only GSM alphabet characters (Latin letters, digits, common punctuation) |
160 characters |
153 characters |
Any other character (š, ć, ž, Cyrillic, emoji, …) |
70 characters |
67 characters |
One non-GSM character switches the whole message to the 70-character limit. The response tells you how many segments your text needed (segments) and what it cost (price). A text that needs more than 6 segments is refused with MESSAGE_TOO_LONG.
Idempotency
Networks fail between the moment you send a request and the moment you read the answer. Idempotency-Key makes a retry safe:
- Send a unique value per business event, for example
order-1042-confirmation.
- Repeating the request with the same key and the same body within 24 hours returns the first response, marked with the header
Idempotent-Replayed: true. Nothing is sent again.
- The same key with a different body is refused with
IDEMPOTENCY_KEY_REUSED.
- While the first request is still running, a second one with the same key gets
409 IDEMPOTENCY_IN_PROGRESS — wait a moment and retry.
After a network error or a 5xx, retry with the same key.
After a 503
503 SMS_SERVICE_UNAVAILABLE on a send means the outcome is unknown: the message may or may not have been accepted. Look it up first:
curl -s "https://api.omaxtelecom.com/sms/messages?reference=order-1042" -H "Authorization: Bearer $TOKEN"
Send again with a new key only if it is not there.
Rejected sends
When a message is refused for a reason other than validation — sender not approved, destination not available, wallet empty — the API answers 422 and still records the message with status rejected. error.details.message_id points to it. Nothing is charged.
Message status
| Status |
Meaning |
Final |
queued |
Accepted and charged, not yet handed to the operator |
|
sent |
Handed to the operator, waiting for a delivery report |
|
delivered |
The operator confirmed delivery to the handset |
✓ |
undelivered |
The message was not delivered |
✓ |
failed |
The operator reported a failure |
✓ |
rejected |
Not accepted and not sent — see error_code |
✓ |
expired |
No delivery report arrived in time |
✓ |
delivered is only ever set from a delivery report. A message that could not be sent is refunded to your wallet.
error_code on a message explains a rejection:
error_code |
Meaning |
INVALID_DESTINATION |
The number in a bulk request cannot be dialed |
INVALID_DESTINATION_OR_NO_ROUTE |
Wrong number, or the destination is not available to you |
INVALID_SENDER |
The sender ID is not approved |
THROTTLED_OR_INSUFFICIENT_FUNDS |
You are sending too fast, or the wallet is empty |
MESSAGE_TOO_LONG |
The text could not be encoded or is too long |
GATEWAY_ERROR |
The gateway could not process the message |
Test mode
An account is either in live or in test mode (GET /wallet → mode). In test mode messages go through the whole API — charging, statuses, webhooks — but are simulated and never reach a phone; they are marked is_test: true. Support switches the mode for you.
Listing messages
GET /messages returns your messages newest first. Filters combine with AND:
| Parameter |
Description |
status |
One of the statuses above |
source |
api or smpp |
to, from, reference |
Exact match |
batch_id |
Messages of one bulk request |
created_from, created_to |
ISO 8601 date-times |
page, per_page |
Paging; per_page up to 200, default 50 |
Messages you submit directly over SMPP also appear in this list, usually within two minutes, with source: "smpp". Their text is not stored and no webhooks are sent for them — their delivery receipts arrive over your SMPP session.
Bulk sending
POST /messages/bulk sends one text to up to 10,000 numbers.
curl -s -X POST https://api.omaxtelecom.com/sms/messages/bulk \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: spring-sale-2026" \
-d '{
"from": "MYSHOP",
"text": "Sale starts today.",
"to": ["+38765123456", "+38766123456"],
"reference": "spring-sale"
}'
The answer is 202 with a batch; the messages are created and sent in the background.
GET /batches/{id} shows the batch status and the number of messages per status.
GET /messages?batch_id={id} lists the individual messages.
GET /batches lists all your batches, newest first.
- A number that cannot be dialed becomes a
rejected message with error_code: "INVALID_DESTINATION"; the other recipients are still sent.
- Batch status
completed means every message was handed over or rejected — not that every message was delivered. Delivery is per message.
For different texts per recipient, send single messages.
Webhooks
Instead of polling, subscribe to SMS events in the Console under Webhooks and we send a signed POST to your HTTPS endpoint whenever something changes:
| Event |
When |
sms.message.sent, sms.message.delivered, sms.message.failed, sms.message.expired |
A message you sent through the API changed status |
sms.batch.completed, sms.batch.failed |
A bulk request finished or stopped |
sms.sender_id.approved, sms.sender_id.rejected |
A sender ID request was reviewed |
sms.wallet.topped_up, sms.wallet.low_balance |
Your SMS balance was topped up or fell below the low balance threshold |
sms.otp.verified, sms.otp.expired |
A one-time code was verified or expired |
sms.id in a message event is the id you got from POST /messages. Endpoints, signature verification, retries and every payload are described in the Webhooks guide.
One-time codes (OTP)
POST /otp/send sends a 6-digit code by SMS and returns a verification_id; POST /otp/verify checks the code the user typed. The OTP message is charged like any other SMS. Rules, examples and a security checklist are in the OTP API guide.
Sender IDs
The sender ID is what the recipient sees as the sender: a name such as MYSHOP (up to 11 letters and digits) or a number of 3 to 15 digits. A sender ID must be approved for your organization before you can send with it.
curl -s -X POST https://api.omaxtelecom.com/sms/sender-ids \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"name": "MYSHOP", "use_case": "Order notifications"}'
A request starts as pending. GET /sender-ids shows the outcome: approved, or rejected with the reviewer's note. A rejected name can be requested again. A clear use_case speeds up the review.
Wallet and prices
curl -s https://api.omaxtelecom.com/sms/wallet -H "Authorization: Bearer $TOKEN"
# { "success": true, "data": { "balance": "152.8", "currency": "EUR", "mode": "live" } }
curl -s https://api.omaxtelecom.com/sms/pricing -H "Authorization: Bearer $TOKEN"
# { "success": true, "data": { "currency": "EUR", "unit": "segment", "rates": [ { "prefix": "38765", "country_code": "BA", "price": "0.05500" } ] } }
Prices are per segment and by destination prefix: the longest prefix that a number starts with applies. A message of 3 segments to a number priced at €0.055 costs €0.165. Amounts are decimal strings — do not parse them as floating-point numbers.
When the wallet is empty, sends are refused with THROTTLED_OR_INSUFFICIENT_FUNDS.
Rate limits
Requests are limited per organization per second. Every response carries:
| Header |
Meaning |
RateLimit-Limit |
Requests allowed per second |
RateLimit-Remaining |
Requests left in the current second |
RateLimit-Reset |
Unix time at which the limit resets |
A 429 adds Retry-After with the number of seconds to wait. For large campaigns use POST /messages/bulk: one request, up to 10,000 recipients.
Errors
| HTTP status |
Error code |
What to do |
401 |
UNAUTHORIZED |
Get a new token |
403 |
ORGANIZATION_REQUIRED |
The token does not identify an organization — check your credentials |
403 |
ORGANIZATION_NOT_FOUND |
Your organization is not registered for SMS — contact support |
403 |
ORGANIZATION_SUSPENDED, ORGANIZATION_INACTIVE |
Contact support |
404 |
NOT_FOUND |
The resource does not exist in your organization |
409 |
SMS_NOT_PROVISIONED |
SMS is not set up for your organization yet |
409 |
IDEMPOTENCY_IN_PROGRESS |
The same key is still being processed; retry shortly |
409 |
SENDER_ID_ALREADY_REQUESTED |
The sender ID is already pending or approved |
422 |
VALIDATION_FAILED |
Fix the request; see error.fields |
422 |
IDEMPOTENCY_KEY_REQUIRED |
Send an Idempotency-Key header |
422 |
IDEMPOTENCY_KEY_REUSED |
Do not reuse a key with a different body |
422 |
MESSAGE_TOO_LONG |
Shorten the text (maximum 6 segments) |
422 |
INVALID_SENDER |
Use an approved sender ID |
422 |
INVALID_DESTINATION_OR_NO_ROUTE |
The number is wrong, or the destination is not available to you |
422 |
THROTTLED_OR_INSUFFICIENT_FUNDS |
Check GET /wallet; if there are funds, slow down and retry |
422 |
GATEWAY_ERROR |
Retry later |
429 |
RATE_LIMITED, OTP_RESEND_TOO_SOON |
Wait for Retry-After seconds |
503 |
SMS_SERVICE_UNAVAILABLE |
Retry with backoff; for sends see After a 503 |
Going live checklist
GET /me answers 200 with sms_enabled: true.
- Your sender ID is
approved (GET /sender-ids).
- Every send carries an
Idempotency-Key, and retries reuse it.
- If you use webhooks, your endpoint verifies the signature, answers quickly with
2xx and de-duplicates on the svix-id header.
- Your code handles
rejected, undelivered, failed and expired, not only delivered.
- You watch the wallet balance (
GET /wallet).
- Your account is in live mode (
GET /wallet → "mode": "live").
Direct SMPP access
High-volume senders can also connect directly over SMPP 3.4 or 5.0, with their own login and delivery receipts on the session. SMPP and the API share one wallet, one price list and one message list. Ask support for SMPP credentials.
Need help?
Email support@omaxtelecom.com. Tell us which endpoint you called, when, and what the response was — including its request_id if it has one. Never send your Client Secret or access token.