To add a voice OTP fallback, keep your SMS flow as it is and add one button: "Call me with the code".
Show it after the SMS resend timer ends (or straight away for landline numbers). When tapped, your server
calls POST https://voice-api.edesy.in/v1/voice-otp/send with the same number and code, then verifies what
the user types as usual. Each answered call costs Rs 1.50.
Get an API key in a few minutes: Open the Voice OTP portal and send a test call to your phone.
What you're building
| Step | Screen or server | What happens |
|---|---|---|
| 1 | Login screen | User enters phone number, you send SMS OTP as today |
| 2 | OTP screen | Resend timer counts down |
| 3 | OTP screen | Timer ends: show "Resend SMS" and "Call me with the code" |
| 4 | Your server | On tap, check your limits, then call the Voice OTP API |
| 5 | User's phone | Call reads the code aloud, twice |
| 6 | Your server | Callback records delivered, no-answer, busy or failed |
| 7 | OTP screen | User types the code; you verify it like any SMS code |
Nothing about verification changes. The voice call is just a second way to deliver the code.
Step 1: Set defaults in the portal
Sign up on the Voice OTP portal, top up the wallet and create an API key (it looks like sk_...). Then set
these once in settings so each API call stays small:
- Language (for example en-IN or Hindi) and voice.
- Message template with
{{otp}}, for example: "Your Acme login code is {{otp}}. I repeat, {{otp}}." - Retries on no-answer (0 to 3).
- Rate limit per number per hour. Keep this low.
- Default callback URL.
Step 2: Decide when to show the button
Good triggers:
- Resend timer ended. The SMS has had 30 to 60 seconds. Offer both "Resend SMS" and "Call me".
- Landline number. If you know the number is a landline (for example, from your own number-type check or because the user told you), skip SMS and call.
- Second SMS failed. After one resend, make the call option the main button.
Don't show the call button before any SMS attempt on a normal mobile login. Voice is the fallback.
Step 3: Call the API
The request is one POST. Reuse the code you already generated and stored for the SMS.
curl -X POST https://voice-api.edesy.in/v1/voice-otp/send \
-H "Authorization: Bearer sk_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"phone_number": "9876543210",
"otp_code": "482913",
"external_id": "login-attempt-8812",
"metadata": { "user_id": 42, "channel": "voice_fallback" }
}'The same call from a Node.js backend, with plain fetch (no SDK needed):
async function sendVoiceOtp({ phone, code, attemptId, userId }) {
const res = await fetch("https://voice-api.edesy.in/v1/voice-otp/send", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.EDESY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
phone_number: phone,
otp_code: code,
external_id: attemptId,
metadata: { user_id: userId, channel: "voice_fallback" },
}),
});
if (res.status === 429) return { ok: false, reason: "too_many_calls" };
if (res.status === 402) return { ok: false, reason: "wallet_empty" };
if (!res.ok) return { ok: false, reason: "provider_error" };
const { data } = await res.json();
return { ok: true, callId: data.id, status: data.status };
}A successful request returns 201 with the call ID, the number, the code and status initiated. If you
leave out otp_code, Edesy generates a 6-digit code and returns it, and you store that one instead.
| Response | Meaning | What to show the user |
|---|---|---|
201 |
Call started | "We're calling you now" |
400 invalid_phone |
Not a valid 10-digit Indian number | Ask them to check the number |
402 insufficient_balance |
Wallet needs a top-up | Generic error; alert your team |
429 rate_limit_exceeded |
Too many calls to this number this hour | "Please wait before requesting another call" |
The full field list is in the Voice OTP docs.
Step 4: Handle the callback
When the call ends, Edesy POSTs the result to your callback URL. Store it against the login attempt so support and your own analytics can see what happened.
// POST /webhooks/voice-otp
app.post("/webhooks/voice-otp", express.json(), async (req, res) => {
const { external_id, status, duration_sec, price } = req.body;
await db.loginAttempts.update(external_id, {
voiceStatus: status, // delivered | no-answer | busy | failed
voiceDurationSec: duration_sec,
voicePrice: price,
});
res.sendStatus(200);
});Reply with any 2xx. If your endpoint fails, Edesy retries. Make the handler idempotent: the same result
may arrive more than once.
On no-answer or busy, the OTP screen can show "We couldn't reach you. Try again in a minute."
Step 5: Verify the code, unchanged
The user types the code. You check it against what you stored, with the expiry you already use. Two details matter for voice:
- Expiry. A call takes longer to arrive and be listened to than an SMS. Make sure the code is still valid for a few minutes after the call starts.
- One valid code. If you issue a new code for the call, invalidate the SMS code. If you reuse the SMS code, both channels lead to the same check.
Abuse controls you need
A "call me" button is an easy target for anyone who wants to make your app phone a stranger repeatedly. Use all of these:
| Layer | Control |
|---|---|
| Edesy | Per-number, per-hour rate limit (set in settings) |
| Your API | Per-user, per-IP and per-device limits on the call endpoint |
| Your UI | Button appears only after an SMS attempt, with its own cooldown |
| Your logic | Only call the number already entered for this login attempt |
| New devices | CAPTCHA or device check before the first call |
What it costs
Rs 1.50 per answered minute, 1-minute minimum, plus 18% GST. Unanswered calls are not charged and there is no monthly fee. Because you only call when SMS hasn't worked, the cost is limited to the logins voice actually saves. Details: Voice OTP pricing.
Test before you ship
- Send a voice OTP to your own phone from the portal.
- Trigger the fallback from your staging login and confirm the callback updates the attempt.
- Let one call ring out and check you get
no-answerand no charge. - Hit the rate limit on purpose and check your UI message.
For background, read how voice OTP works and voice OTP vs SMS OTP. The product overview is on the Voice OTP page.
Create a Voice OTP account and add the fallback to your staging login today.