API reference
Two endpoints. Bearer auth with a key you mint in Settings, JSON in and JSON out, and the same error envelope as the rest of the CosmoOps platform.
Authentication
Every request carries a key from Settings → API keys. A key looks like msg_live_… or msg_test_… — the mode is in the token so a key pasted into the wrong environment is visibly the wrong key.
Authorization: Bearer msg_live_...
A test key authenticates and cannot send. Use one to prove your auth, your request shape and your error handling before any message is charged; it answers 403 test_key on a send and works normally for reads.
Keys carry scopes: sms:send and sms:read. They are separate so a key embedded in a dashboard that renders delivery status cannot also spend your balance.
Send a message
POST /api/v1/sms/send
Authorization: Bearer msg_live_...
Content-Type: application/json
Idempotency-Key: 7f3c1e94-...
{
"templateId": "tpl_...",
"recipient": "9876543210",
"values": ["Priya", "482913"]
}values fill the template’s {#var#} placeholders in order. DLT placeholders are positional and identical — there is nothing in the format that says which hole is a name and which is an order number, so the order is the whole contract. A value may be at most 30 characters and may not contain a line break.
recipient is an Indian mobile number with or without the country code; we normalise it. Anything else is rejected rather than attempted.
Send to several — up to 100 in one call
{
"templateId": "tpl_...",
"recipients": [
{ "recipient": "9876543210", "values": ["Priya", "482913"] },
{ "recipient": "9000000001", "values": ["Raj", "118204"] }
]
}Send either recipient or recipients, not both. Both forms answer the same body, so a client library needs no branch.
The response
200 OK
{
"accepted": 1,
"failed": 0,
"segments": 1,
"costPaise": 25,
"results": [
{ "recipient": "+919876543210", "ok": true,
"messageId": "a3f1...", "status": "sent" }
]
}Partial success answers 200. A batch where sixty went and forty were refused is a 200 with both counts and per-recipient results — you asked for a hundred attempts and got a hundred attempts. Answering 4xx would invite you to re-send the whole batch and be charged twice for the part that worked.
A refusal that applies to every recipient — no DLT registration, an unapproved template, an exhausted balance — is a 422, because nothing was attempted and there is one thing to fix.
costPaise is paise, and segments is message parts. ₹0.25 per message part — a body over 160 characters is more than one part.
Idempotency
Send an Idempotency-Key. Without one, a retried request sends and charges twice — a message cannot be un-sent and a balance cannot be un-spent. With one, a retry replays the original response, including its status code.
Keys are honoured for 24 hours. Reusing a key with a different body is a 422 rather than the first request’s answer — otherwise you would believe the second message was sent. A key whose request is still in flight answers 409.
Delivery status
GET /api/v1/sms/{messageId}
Authorization: Bearer msg_live_...
{
"id": "a3f1...",
"recipient": "+919876543210",
"sender": "ACMEIN",
"status": "delivered",
"segments": 1,
"costPaise": 25,
"deliveredAt": "2026-10-15T09:41:02.000Z",
"events": [
{ "status": "sent", "providerStatus": "submitted", ... },
{ "status": "delivered", "providerStatus": "delivered", ... }
]
}status is one of queued, sent, delivered, failed or unknown — the last meaning the carrier accepted it and never reported either way.
events is the authoritative record. Delivery reports arrive out of order, so status is a projection of them and can briefly lag. If you are reconciling your own records, read the events; if you are rendering a badge, read the status.
Errors
{ "error": { "code": "cannot_send", "message": "..." } }code is the stable half and is what you should branch on; message is prose and may be reworded.
401 unauthorized— the key is missing, malformed, unknown, or lacks the scope. Deliberately one answer for all four: distinguishing them would confirm a guessed key.401 key_revoked/key_expired— distinguished, because you hold a valid secret and can act on it.403 test_key— a test key attempted a send.422 cannot_send— the request is fine and the account state refuses it.429 rate_limited— slow down; the response carriesRetry-After.
What there is no endpoint for
There is no webhook for delivery reports yet. We poll our carrier and you poll us — GET /api/v1/sms/{messageId} is the whole delivery story from your side. Pushing to your endpoint is worth doing and is not built, and saying so is better than you discovering it after writing a receiver.
There is also no endpoint for creating templates or sender IDs. Both need an operator’s approval and a human decision about what text to register, so they are done in the app rather than by an API.