A voice OTP is a one-time code delivered by phone call instead of SMS. Your server asks Edesy to call a number, Edesy calls it, reads the code aloud from your template (with optional press-1 confirmation), then posts the result to your callback. The user types the code into your app as usual. It costs Rs 1.50 per answered minute, and unanswered calls are free.
Try the flow on your own phone: Open the Voice OTP portal, create an API key and call yourself.
The flow in seven steps
- The user asks for a code. They enter a phone number on your login, sign-up or checkout screen.
- Your server calls the API.
POST https://voice-api.edesy.in/v1/voice-otp/sendwith the phone number and, optionally, your own code. - Checks run. Edesy checks your wallet balance and the per-number rate limit. If either fails, the API
returns an error straight away (
402 insufficient_balanceor429 rate_limit_exceeded) and no call is made. - The call goes out. The response comes back with an ID, the code and the status
initiated. - The user answers and hears the code. The template is spoken by text-to-speech, with
{{otp}}replaced by the code. If press-1 confirmation is on, the user is asked to press a key. - The call ends. Edesy records the result: delivered, no-answer, busy or failed, plus the key pressed, duration and hangup cause.
- Your callback gets the result. Your server can show "Call me again" or switch to another channel if the call wasn't answered.
Your app still owns verification: you store the code (or a hash of it) with an expiry, and compare it with what the user types.
What goes into the request
| Field | Required | What it does |
|---|---|---|
phone_number |
Yes | 10-digit Indian number; a leading +91, 91 or 0 is removed |
otp_code |
No | Your own 4 to 10 character code; leave it out for a generated 6-digit code |
message_template |
No | Spoken text with {{otp}}; falls back to your saved default |
language |
No | For example en-IN, en-US or Hindi |
gather_enabled |
No | Turns on press-1 confirmation; the key comes back as dtmf_response |
callback_url |
No | Where the result is posted; overrides your default |
external_id |
No | Your own reference, returned in the callback and searchable in logs |
metadata |
No | Any JSON you want echoed back |
Authentication is Authorization: Bearer sk_.... The full reference is in the
Voice OTP docs.
The spoken message
The template is plain text. A good OTP template does three things: says who is calling, reads the code slowly, and repeats it.
- "Your Acme login code is {{otp}}. I repeat, {{otp}}."
- Hindi: "Aapka Acme login code hai {{otp}}. Dobara sunein, {{otp}}."
Text-to-speech reads each digit separately, so the user can write it down. Keep the brand name short and don't add offers or extra lines. Every sentence adds seconds to the call, and OTP calls should stay transactional.
You set a default template, language and voice once in the portal, then override per request only when you need to (for example, a different brand in a multi-brand app).
Press-1 confirmation
With gather_enabled on, the call asks the user to press a key after hearing the code. The key comes back
as dtmf_response in the callback and log. This tells you a person heard the code, not just that the call
connected. It's useful for high-value actions such as changing bank details or approving a payout, where
you want a little more proof.
Leave it off for routine logins: it adds a few seconds and one more step.
Retries, rate limits and expiry
| Control | What it does | Where you set it |
|---|---|---|
| Retries on no-answer | Calls again if the first attempt isn't answered | Settings (0 to 3) |
| Rate limit per number | Caps how many OTP calls one number can get per hour | Settings |
| Default callback URL | Where results go when the request doesn't name one | Settings |
| Code expiry | How long the code is valid | Your app |
The rate limit matters more than it looks. Without it, anyone can type a victim's number into your login form and make your app call them repeatedly. That annoys the victim and costs you money. Keep the limit low.
The callback and logs
When the call finishes, Edesy posts JSON like this to your callback URL:
{
"id": "5b1c2d3e-0000-4000-8000-000000000000",
"external_id": "login-8812",
"phone_number": "9876543210",
"status": "delivered",
"dtmf_response": "1",
"duration_sec": 18,
"price": "1.5000",
"completed_at": "2026-10-28T10:15:30Z"
}Status is one of delivered, no-answer, busy or failed. In the portal, each call has a log with a
timeline (initiated, ringing, answered, completed), the key pressed and the hangup cause, so support can
answer "I never got the call" with facts.
What it costs
| Item | Price |
|---|---|
| Answered OTP call | Rs 1.50 per minute, 1-minute minimum |
| Unanswered, busy or failed call | Not charged |
| Monthly fee | None |
| GST | 18% extra, with GST invoices |
Calls are paid from a prepaid INR wallet. The price of each call is in the callback and the log, so you can
reconcile per user or per external_id. See Voice OTP pricing.
Where voice OTP fits
Voice OTP is a strong fallback for SMS and a strong primary channel for landline and feature-phone users. For the cost and DLT comparison, read voice OTP vs SMS OTP. For the code to wire it into an existing login, read add voice OTP fallback to your login. If you're comparing against missed-call verification, see missed call verification vs OTP.
More on the product: Voice OTP.
Create a Voice OTP account and send a test code to your own phone.