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 carries Retry-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.