OTP SMS · India · ₹0.30 per message

OTP Delivery API

Deliver the login codes your app generates by SMS, through Niyantha's DLT-approved template and sender. Register an app name, make one REST call per OTP, and see every delivery's status and timing on your dashboard.

EndpointPOST https://api.niyantha.in/api/v1/s2s/accounts/otp/deliveries
AuthX-API-Key / X-API-Secret
Price₹0.30 / accepted SMS
Step 1

Setup (once per app)

  1. Sign in to niyantha.in (your product's admin signs up with a mobile number; the same account owns the wallet, the keys and the deliveries).
  2. Open the OTP delivery tab and register your app name exactly as it should appear in the SMS (2–30 characters). A Niyantha admin reviews it; you see the result in the tab and receive a realtime otp.app.reviewed event.
  3. Create an S2S key in Profile → API keys (POST /api/v1/session/keys, pick a long ttl_seconds). The secret is shown once. See the S2S guide.
  4. Fund the wallet (or ask Niyantha for SMS credit). Each accepted SMS costs ₹0.30; the tab shows how many OTPs you can still send.

The SMS text is fixed by the operator-registered template; only your app name and the code change:

What the recipient sees
Your <App name> login code is 482913. It expires in 10 minutes. Do not share it with anyone. From niyantha.in
Step 2

Send an OTP

POST
https://api.niyantha.in/api/v1/s2s/accounts/otp/deliveries

Bill and send one OTP. Your backend generates and verifies the code; Niyantha only delivers it.

Request
POST /api/v1/s2s/accounts/otp/deliveries HTTP/1.1
Host: api.niyantha.in
Content-Type: application/json
X-API-Key: <public key>
X-API-Secret: <secret>

{
  "app_name": "Uexcess",
  "otp": "482913",
  "contact": "+91 94831 59238",
  "reference": "login-10293"
}
app_namestring, requiredAn app name you registered that an admin approved. Case/spacing-insensitive match.
otpstring, requiredThe code you generated: 4–10 letters or digits. Niyantha never stores it.
contactstring, requiredIndian mobile: 10 digits starting 6–9, with or without +91 / 91 / 0. Stored as +91XXXXXXXXXX.
referencestring, optionalYour own id for this OTP (≤120 chars). Repeating it returns the earlier delivery (200) instead of sending again.

Always send a reference (for example your own OTP/challenge id). If your request times out and you retry, the same reference guarantees the person gets one SMS and you pay once.

Contract

Response & statuses

201 Created — accepted by the gateway
{
  "id": 1203,
  "app_id": 4,
  "app_name": "Uexcess",
  "contact": "+919483159238",
  "otp_length": 6,
  "reference": "login-10293",
  "status": "sent",
  "provider": "msg91",
  "provider_request_id": "3763646c3058373530393938",
  "requested_at": 1791273000,
  "sent_at": 1791273000,
  "provider_latency_ms": 238,
  "price_paise": 30,
  "charged_paise": 30,
  "from_credit_paise": 30,
  "from_balance_paise": 0,
  "transaction_id": 88211,
  "api_key_id": 17,
  "api_key_name": "uexcess-prod"
}
Later, after the operator's delivery report (GET …/deliveries/1203 or the otp.delivery.updated event)
{
  "id": 1203,
  "status": "delivered",
  "delivered_at": 1791273006,
  "delivery_ms": 6000,
  "provider_status_desc": "DELIVERED",
  "...": "all other fields as above"
}
statussending → sent → delivered | failed. 'sent' = provider accepted; 'delivered' = handset report arrived.
provider_latency_msHow long the SMS gateway took to accept the message.
delivery_msHandset delivery time measured from your request (set with the delivery report; whole-second resolution).
charged_paise / from_credit_paise / from_balance_paiseWhat was billed and from where (SMS credit first, wallet balance second).
refund_transaction_idPresent when the provider refused the message and the charge was reversed.
failure_reasonWhy a delivery failed: a provider error, or 'operator: NDNC' style reasons from the delivery report.
provider_request_idThe gateway's message id, useful when raising a ticket.

HTTP status codes

CodeErrorMeaning
201CreatedThe OTP was accepted by the SMS gateway; you are billed.
200OKSame reference seen before: the earlier delivery is returned, nothing is sent or billed.
400ValidationError / NotFound / ForbiddenBad payload, unknown app name, or app name not approved yet.
402PaymentRequiredSMS credit + wallet balance cannot cover one OTP. Top up at niyantha.in.
429RateLimitErrorMore than 5 OTPs to the same number within 10 minutes.
502ProviderErrorThe SMS gateway refused the message. The delivery is stored as failed and nothing is charged.

Errors are JSON: { "error": "PaymentRequired", "message": "insufficient SMS credit and wallet balance for a ₹0.30 OTP; add money to your Niyantha wallet" }.

S2S + dashboard

Read deliveries and app names

GET
https://api.niyantha.in/api/v1/s2s/accounts/otp/deliveries?page=1&count=20&status=delivered&app_id=&contact=59238

Newest first; { items, total_count, page, count, has_more }. count ≤ 100.

GET
https://api.niyantha.in/api/v1/s2s/accounts/otp/deliveries/:id

One delivery with its timeline and charge split.

GET
https://api.niyantha.in/api/v1/s2s/accounts/otp/apps

Your app names with status (pending | approved | rejected | suspended) and sample_preview.

The dashboard's OTP delivery tab uses the same data over the realtime connection (account identity): otp.overview, otp.apps.list, otp.app.apply, otp.deliveries.list, otp.delivery.get. Two events are pushed to the account's sockets:

EVENT
otp.app.reviewed

{ app, account_id } after an admin approves, rejects or suspends an app name.

EVENT
otp.delivery.updated

{ delivery } when a delivery is sent, fails, or receives a delivery report.

Sending is deliberately S2S-only, so a browser session never needs to hold an API secret.

₹0.30

Pricing & billing

  • Flat ₹0.30 per SMS the gateway accepts (statuses sent, delivered, and operator-reported failures such as DND numbers). Gateway refusals (502) are reversed immediately.
  • Charged to the wallet's SMS credit first and the rupee balance second, never partially: an OTP is refused with 402 if the two together cannot cover ₹0.30.
  • Each delivery links to its ledger row (transaction_id, type consume_sms_otp) in the wallet's transaction history; refunds are refund_sms_otp.
  • Limits: 5 OTPs per mobile number per 10 minutes per account, up to 10 app names per account. Need more? Talk to us.
Samples

Copy-paste integrations

Go
body, _ := json.Marshal(map[string]string{"app_name": "Uexcess", "otp": code, "contact": phone, "reference": challengeID})
req, _ := http.NewRequest(http.MethodPost, "https://api.niyantha.in/api/v1/s2s/accounts/otp/deliveries", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-API-Key", os.Getenv("NIYANTHA_KEY"))
req.Header.Set("X-API-Secret", os.Getenv("NIYANTHA_SECRET"))
resp, err := http.DefaultClient.Do(req)
// 201/200 ok · 402 top up · 429 slow down · 502 provider refused (not charged)
Node (fetch)
const res = await fetch("https://api.niyantha.in/api/v1/s2s/accounts/otp/deliveries", {
  method: "POST",
  headers: { "Content-Type": "application/json", "X-API-Key": process.env.NIYANTHA_KEY, "X-API-Secret": process.env.NIYANTHA_SECRET },
  body: JSON.stringify({ app_name: "Uexcess", otp: code, contact: phone, reference: challengeId }),
});
const delivery = await res.json(); // delivery.status === "sent"
Python (requests)
r = requests.post("https://api.niyantha.in/api/v1/s2s/accounts/otp/deliveries",
    json={"app_name": "Uexcess", "otp": code, "contact": phone, "reference": challenge_id},
    headers={"X-API-Key": os.environ["NIYANTHA_KEY"], "X-API-Secret": os.environ["NIYANTHA_SECRET"]}, timeout=15)
r.raise_for_status()  # 402 → ask the account owner to top up the Niyantha wallet
curl
curl -X POST https://api.niyantha.in/api/v1/s2s/accounts/otp/deliveries \
  -H "Content-Type: application/json" -H "X-API-Key: $NIYANTHA_KEY" -H "X-API-Secret: $NIYANTHA_SECRET" \
  -d '{"app_name":"Uexcess","otp":"482913","contact":"+919483159238","reference":"login-10293"}'