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.
POST https://api.niyantha.in/api/v1/s2s/accounts/otp/deliveriesX-API-Key / X-API-Secret₹0.30 / accepted SMSSetup (once per app)
- 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).
- 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.reviewedevent. - Create an S2S key in Profile → API keys (
POST /api/v1/session/keys, pick a longttl_seconds). The secret is shown once. See the S2S guide. - 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:
Your <App name> login code is 482913. It expires in 10 minutes. Do not share it with anyone. From niyantha.inSend an OTP
https://api.niyantha.in/api/v1/s2s/accounts/otp/deliveriesBill and send one OTP. Your backend generates and verifies the code; Niyantha only delivers it.
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.
Response & statuses
{
"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"
}{
"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
| Code | Error | Meaning |
|---|---|---|
| 201 | Created | The OTP was accepted by the SMS gateway; you are billed. |
| 200 | OK | Same reference seen before: the earlier delivery is returned, nothing is sent or billed. |
| 400 | ValidationError / NotFound / Forbidden | Bad payload, unknown app name, or app name not approved yet. |
| 402 | PaymentRequired | SMS credit + wallet balance cannot cover one OTP. Top up at niyantha.in. |
| 429 | RateLimitError | More than 5 OTPs to the same number within 10 minutes. |
| 502 | ProviderError | The 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" }.
Read deliveries and app names
https://api.niyantha.in/api/v1/s2s/accounts/otp/deliveries?page=1&count=20&status=delivered&app_id=&contact=59238Newest first; { items, total_count, page, count, has_more }. count ≤ 100.
https://api.niyantha.in/api/v1/s2s/accounts/otp/deliveries/:idOne delivery with its timeline and charge split.
https://api.niyantha.in/api/v1/s2s/accounts/otp/appsYour 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:
otp.app.reviewed{ app, account_id } after an admin approves, rejects or suspends an app name.
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.
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
402if the two together cannot cover ₹0.30. - Each delivery links to its ledger row (
transaction_id, typeconsume_sms_otp) in the wallet's transaction history; refunds arerefund_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.
Copy-paste integrations
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)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"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 walletcurl -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"}'