# Kusha SMS — API Manual (full text) > The complete REST API reference for Kusha SMS (https://kushasms.com), a bulk SMS > platform for Nepal operated by Sahasra Tech. Send campaigns, alerts and OTPs to > NTC, Ncell and Smart Telecom numbers. > > Human version: https://kushasms.com/api-docs > OpenAPI 3.1 spec: https://kushasms.com/openapi.json > Site index: https://kushasms.com/llms.txt ## Read this first 1. **Every endpoint answers HTTP 200** — including for a bad token, a missing field, an empty wallet and a rate-limit rejection. Decide success from the `error` field in the JSON body, never from the status code. 2. **Sends can partially succeed.** `data.valid` lists messages that were queued and charged; `data.invalid` lists recipients that were aborted and not charged. 3. **There is no sandbox.** Every accepted message is delivered to a real handset on a real network and spends real credit. Send test messages only to numbers you control, and read the testing checklist at the end before your first send. 4. **Prefer v4.** v3 and v1 exist for compatibility with older integrations. Base URL: `https://kushasms.com` ## Authentication Tokens are created in the portal under Developers -> API Tokens, behind a PIN. The raw token is shown only once — it is stored hashed, so a lost token is replaced, not recovered. A token can spend credit: keep it server-side, never in a mobile app, browser bundle or public repository. Revoking a token takes effect immediately. - v4 endpoints: `auth-token: ` request header. - v3 and v1 endpoints: `auth_token` form field or query parameter. API sends draw from the **API SMS** wallet, which is separate from the web/portal wallet. ## Endpoints ### POST /sms/v4/send-user — send (recommended) Headers: `auth-token`, `Content-Type: application/json`, optional `Idempotency-Key`. Body: { "to": ["98XXXXXXXX", "98YYYYYYYY"], "text": ["Hello from Kusha SMS"] } One `text` broadcasts to every number. N texts pair index-for-index with N numbers; extra entries on either side are dropped. Success (HTTP 200): { "responses": [{ "error": false, "message": "2 messages have been queued for delivery.", "data": { "valid": [{ "id": "12_3456", "mobile": "97798XXXXXXXX", "text": "Hello from Kusha SMS", "credit": 1, "network": "ntc", "status": "queued", "shortcode": "KUSHA" }], "invalid": [] } }], "errors": [] } `id` is `"_"`. Failure — no recipient survived validation, or the wallet could not fund the whole batch (funding is all-or-nothing): { "error": true, "message": "All messages encountered errors.", "errors": [{"error": true, "message": "No valid recipients.", "data": {"valid": [], "invalid": [...]}}] } curl: curl -X POST https://kushasms.com/sms/v4/send-user \ -H "auth-token: " \ -H "Content-Type: application/json" \ -d '{"to":["98XXXXXXXX"],"text":["Hello from Kusha SMS"]}' ### POST or GET /sms/v3/send — send (legacy) Parameters: `auth_token`, `to` (comma-separated numbers), `text`. curl -X POST https://kushasms.com/sms/v3/send \ -d "auth_token=" \ -d "to=98XXXXXXXX,98YYYYYYYY" \ -d "text=Hello from Kusha SMS" Success: { "error": false, "message": "2 messages has been queued for delivery.", "data": { "valid": [{"id": 3456, "mobile": "97798XXXXXXXX", "text": "Hello", "credit": 1, "network": "ncell", "status": "queued"}], "invalid": [] } } Errors are flat: `{"error": true, "message": "Not enough balance.", "data": []}`. ### GET /sms/v4/available-credit — spendable credit Header `auth-token`. The cheapest pre-flight check. {"available_credit": 750.0, "response_code": 200} `available_credit` is what you can actually send right now. If your account is supplied by a reseller it is the smaller of your own allowance and your provider's remaining credit, so it can fall without you sending anything. Size batches against this number. ### POST /sms/v4/credit — credit summary Header `auth-token`. { "available_credit": 750.0, "total_sms_sent": 1240, "last_transaction_date": "2026-08-07 14:05:11", "last_transaction_date_sms_sent": 0, "response_code": 202 } ### POST /sms/v1/credit — credit summary (legacy) Parameter `auth_token`. Returns `available_credit`, `total_sms_sent`, `response_code`. ### POST /sms/v4/api-report — delivery report Header `auth-token`. Optional filters: `start_date` and `end_date` (`YYYY-MM-DD`), `network` (`ntc` | `ncell` | `smarttel`), `mobile` (substring). Up to 500 rows, newest first. { "error": false, "total": 128, "credits": 131.0, "data": [{"id": 3456, "mobile": "98XXXXXXXX", "text": "Hello", "credit": 1.0, "network": "ntc", "status": "delivered", "sent_on": "2026-08-07 14:05:11"}], "response_code": 200 } `credits` is the NET charged total: a refunded message counts zero, so the figure reconciles with the wallet rather than overstating spend. ### POST /sms/v1/report/api — paged log (legacy) Parameters `auth_token`, `page` (1-based). 50 rows per page. Returns `error`, `page`, `per_page`, `total`, `data`. ## Idempotency Network timeouts are the one realistic way to send twice. Put a unique `Idempotency-Key` header on a v4 send; a retry with the same key returns the first response instead of dispatching again. curl -X POST https://kushasms.com/sms/v4/send-user \ -H "auth-token: " \ -H "Idempotency-Key: order-4821-otp" \ -H "Content-Type: application/json" \ -d '{"to":["98XXXXXXXX"],"text":["Your code is 4821"]}' The key is claimed before the send, so two concurrent requests can never both dispatch. If the first is still in flight the second returns `"A request with this Idempotency-Key is already being processed."` — retry shortly. Keys are scoped to your account; use one per business event. ## Rate limits 60 requests per minute per token by default, in fixed one-minute windows; your plan may set a different figure. Over the limit: {"error": true, "message": "Rate limit exceeded. Try again shortly.", "data": []} still with HTTP 200. Batch recipients into one v4 call rather than one call per number. ## Numbers and credits Numbers are canonicalised to the 10-digit Nepali form. `+977 98-1234567`, `97798XXXXXXXX` and `98XXXXXXXX` are the same number; responses echo it with the `977` country code. Anything that is not a 10-digit number starting with `9` is aborted and not charged. Numbers on your account's blocklist are aborted the same way. Credit is one per segment: | Encoding | Single message | Per part when concatenated | |--------------------------|----------------|----------------------------| | GSM-7 | 160 characters | 153 | | UCS-2 (Nepali, emoji) | 70 characters | 67 | A single character outside GSM-7 switches the whole message to UCS-2. The characters `^ { } [ ] ~ | \ €` cost two GSM-7 characters each. ## Delivery statuses | Status | Meaning | Charged | |-------------|---------------------------------------------------|-----------| | `queued` | Accepted, awaiting dispatch | Reserved | | `submitted` | Handed to the operator | Yes | | `sent` | Operator accepted it | Yes | | `delivered` | Confirmed on the handset | Yes | | `failed` | Never reached an operator | Refunded | | `cancelled` | Cancelled before dispatch | Refunded | | `aborted` | Never accepted (bad or blocked number, empty text)| No | `submitted` is terminal for billing. Once an operator has taken a message we are charged for it, so it is charged to you: it stays `submitted` even if the operator later reports it undelivered, and it is never refunded from that point. Only a message no operator ever took comes back as `failed`, and only those are refunded and retryable. ## Webhooks Enable a callback URL under Developers -> Webhooks. Every delivery receipt is POSTed as JSON: POST https://your-app.example/kusha-hook X-Kusha-Event: dlr X-Kusha-Signature: { "message_id": 3456, "to": "98XXXXXXXX", "network": "ntc", "status": "delivered", "smpp_message_id": "0f1a2b", "delivered_at": "2026-08-07T14:05:11+05:45" } Verify by recomputing `HMAC-SHA256(secret, raw_body)` and comparing with `X-Kusha-Signature` in constant time — use the raw bytes, not re-serialised JSON. Events are `dlr`, `inbound` and `batch`. Delivery is retried up to five times; answer `2xx` quickly and do your work afterwards. The URL must be publicly resolvable — loopback and private addresses are refused. ## Error messages | Message | Cause | |------------------------------------------------|-----------------------------------------------------------| | `The auth token field is required.` | No `auth-token` header and no `auth_token` parameter | | `The provided Auth Token is not valid.` | Unknown or revoked token | | `The to field is required.` | Missing recipients | | `The text field is required.` | Missing message text | | `Not enough balance.` | The API wallet cannot fund the whole batch | | `Rate limit exceeded. Try again shortly.` | Over the per-minute budget | | `All messages encountered errors.` | v4: no valid recipient, or funding failed | ## Testing checklist There is no sandbox. Work through this in order; only step 3 spends credit. 1. **Check the token.** `GET /sms/v4/available-credit`. A valid token returns a number; an invalid one returns `{"error": true, "message": "The provided Auth Token is not valid.", "data": []}`. 2. **Exercise the failure path for free.** Send to a deliberately malformed number such as `"12345"`. It comes back under `invalid` with `status: "aborted"` and `credit: 0`, and nothing is charged. 3. **Send one real message** to a number you control, with an `Idempotency-Key`. Confirm `valid[0].status == "queued"`. 4. **Replay the same request** with the same key. You should get the same response and only one message on the handset. 5. **Confirm delivery** with `POST /sms/v4/api-report` for today's date, or via your webhook. Status should reach `delivered`. 6. **Reconcile.** Call `available-credit` again — it should have dropped by exactly the credits reported for the send. Support: info@sahasratech.com.np