SMS API Manual

Send SMS to NTC, Ncell and Smart Telecom numbers in Nepal from your own application. Token authentication, JSON responses, delivery reports and signed webhooks.

Overview

Base URLhttps://kushasms.com
Authauth-token header (v4) or auth_token parameter (v3/v1)
FormatJSON responses; JSON or form-encoded requests
WalletAPI sends draw from your API SMS credit, not the web wallet
Machine-readable/openapi.json · /llms-full.txt · /llms.txt

Create a token in the portal under Developers → API Tokens (protected by a PIN you set once). The raw token is shown only when it is created — it is stored hashed, so a lost token has to be replaced rather than recovered.

Authentication

Every request carries the token one of two ways. v4 endpoints read the header; v3 and v1 also accept it as a parameter, so a browser or a shell one-liner works:

# v4 — header
curl -X POST https://kushasms.com/sms/v4/send-user \
  -H "auth-token: <YOUR_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"to":["98XXXXXXXX"],"text":["Hello from Kusha SMS"]}'

# v3 — parameter
curl -X POST https://kushasms.com/sms/v3/send \
  -d "auth_token=<YOUR_TOKEN>" \
  -d "to=98XXXXXXXX" \
  -d "text=Hello from Kusha SMS"
Keep tokens server-side. A token can spend your credit; never ship one in a mobile app, browser bundle or public repository. Revoke a leaked token from the portal — revocation takes effect immediately.

Response conventions

Two things surprise people, so they are worth stating plainly:

v3/v1 use a flat envelope (error, message, data); v4 wraps successes in responses[] and failures in errors[].

For AI agents

This manual has machine-readable twins, kept in step with the code:

  • /openapi.json — OpenAPI 3.1 description of every endpoint; load it as a tool definition.
  • /llms-full.txt — this entire manual as plain Markdown, in one request.
  • /llms.txt — short index of the site.

Before sending anything, read the testing checklist. There is no sandbox: every accepted message is delivered to a real handset and charged. Verify with GET /sms/v4/available-credit first, then send a single message to a number you control.

Send · v4

POST/sms/v4/send-user

Arrays in, per-recipient result out. One text broadcasts to every number; N texts pair index-for-index with N numbers (extra entries on either side are dropped). Headers: auth-token, optional Idempotency-Key.

// request
{
  "to":   ["98XXXXXXXX", "98YYYYYYYY"],
  "text": ["Hello world"]
}

// response — 200 OK
{
  "responses": [{
    "error": false,
    "message": "2 messages have been queued for delivery.",
    "data": {
      "valid": [{
        "id": "12_3456",          // "<tenant>_<message>"
        "mobile": "97798XXXXXXXX",
        "text": "Hello world",
        "credit": 1,
        "network": "ntc",
        "status": "queued",
        "shortcode": "KUSHA"
      }],
      "invalid": []
    }
  }],
  "errors": []
}

If no recipient survives validation, or the wallet can't fund the whole batch, the reply is {"error": true, "message": "All messages encountered errors.", "errors": [...]} and nothing is charged.

Send · v3

POSTGET/sms/v3/send

One text to a comma-separated list. Parameters: auth_token, to, text.

POST /sms/v3/send   auth_token=…&to=98XXXXXXXX,98YYYYYYYY&text=Hello

// response — 200 OK
{
  "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": []
  }
}

Credit

GET/sms/v4/available-credit

The cheapest pre-flight check — use it before every batch.

{ "available_credit": 750.0, "response_code": 200 }
POST/sms/v4/credit
{
  "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

Legacy shape: available_credit, total_sms_sent, response_code. Authenticates with auth_token.

What available_credit means. It 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.

Reports

POST/sms/v4/api-report

Filters: start_date, end_date (YYYY-MM-DD), network (ntc, ncell, smarttel), mobile (substring). Returns up to 500 rows, newest first.

{
  "error": false,
  "total": 128,
  "credits": 131.0,        // net charged — refunded messages count 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
}
POST/sms/v1/report/api

Paged log: send page (1-based, 50 per page). Returns error, page, per_page, total, data.

Idempotency

Network timeouts are the one way to accidentally send twice. Put a unique Idempotency-Key header on a v4 send and a retry with the same key returns the first response instead of sending again:

curl -X POST https://kushasms.com/sms/v4/send-user \
  -H "auth-token: <YOUR_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 you get {"error": true, "message": "Rate limit exceeded. Try again shortly.", "data": []} — still HTTP 200. Batch recipients into one v4 call rather than one call per number.

Numbers & 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 isn't a 10-digit number starting with 9 is aborted, not charged.

Credit is one per segment. Plain English is GSM-7; a single Devanagari character (or any non-GSM-7 character) switches the whole message to UCS-2:

EncodingSingle messagePer part when longer
GSM-7160 characters153
UCS-2 (Nepali, emoji)70 characters67

The characters ^ { } [ ] ~ | \ € cost two GSM-7 characters each. To check a message before you send it, paste it into the SMS character counter — it applies these exact rules and explains how Nepali text is counted.

Delivery statuses

A send returns queued; the status moves as the operator takes the message. Poll a report endpoint or take the webhook.

StatusMeaningCharged
queuedAccepted, awaiting dispatchReserved
submittedHanded to the operatorYes
sentOperator accepted itYes
deliveredConfirmed on the handsetYes
failedNever reached an operatorRefunded
cancelledCancelled before dispatchRefunded
abortedNever accepted (bad/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 and every delivery receipt is POSTed to it as JSON:

POST https://your-app.example/kusha-hook
X-Kusha-Event: dlr
X-Kusha-Signature: <hex HMAC-SHA256 of the raw body>

{
  "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 — compare in constant time, and use the raw bytes, not re-serialised JSON. Events are dlr, inbound and batch. Delivery is retried up to five times; respond 2xx quickly and do the work afterwards. The URL must be public — private and loopback addresses are refused.

Errors

MessageCause
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 can't fund the whole batch — it is all-or-nothing
Rate limit exceeded. Try again shortly.Over your per-minute budget
All messages encountered errors.v4: no recipient survived validation, or funding failed

Testing checklist

There is no sandbox. Every accepted message goes to a real handset on a real network and spends real credit. Test against a number you own.
  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."}.
  2. Check the failure paths without spending anything. Send with a deliberately malformed number (e.g. "12345"): it comes back under invalid with status: "aborted" and credit: 0.
  3. Send one real message to your own number, with an Idempotency-Key. Confirm valid[0].status == "queued".
  4. Replay the same request with the same key and confirm you get the same response and only one message on the handset.
  5. Confirm delivery via POST /sms/v4/api-report with today's date, or your webhook.
  6. Reconcile. Call available-credit again — it should have dropped by exactly the credits reported for the send.

Need help? info@sahasratech.com.np