# How to Add a Voice OTP Fallback to Your Login (2026)

> Step-by-step: add a 'Call me with the code' voice OTP fallback to an SMS login. When to offer it, the API call, the callback handler, code expiry and abuse limits, with cURL and Node examples.

Source: https://edesy.in/blog/add-voice-otp-fallback-to-login-2026

**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](https://voice-otp.edesy.in/?utm_source=edesy.in&utm_medium=blog&utm_campaign=add-voice-otp-fallback-to-login-2026) 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.

```bash
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):

```js
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](https://edesy.in/docs/voice-otp).

## 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.

```js
// 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](https://edesy.in/voice-otp/pricing).

## Test before you ship

1. Send a voice OTP to your own phone from the portal.
2. Trigger the fallback from your staging login and confirm the callback updates the attempt.
3. Let one call ring out and check you get `no-answer` and no charge.
4. Hit the rate limit on purpose and check your UI message.

For background, read [how voice OTP works](https://edesy.in/blog/how-voice-otp-works-2026) and
[voice OTP vs SMS OTP](https://edesy.in/blog/voice-otp-vs-sms-otp-india-2026). The product overview is on the
[Voice OTP page](https://edesy.in/voice-otp).

[Create a Voice OTP account](https://voice-otp.edesy.in/?utm_source=edesy.in&utm_medium=blog&utm_campaign=add-voice-otp-fallback-to-login-2026)
and add the fallback to your staging login today.

## Frequently Asked Questions

### When should the voice OTP option appear?

After the SMS has had a fair chance to arrive. A common pattern is to show a 'Call me with the code' button once the resend timer runs out (often 30 to 60 seconds), and to offer it straight away if the user entered a landline number.

### Should the voice call read the same code as the SMS?

Either works. Reusing the SMS code is simpler and means either channel's code is accepted. Issuing a new code is stricter; if you do, invalidate the SMS code so only the latest one works.

### Do I need an SDK to call the Voice OTP API?

No. It is a single HTTPS POST to https://voice-api.edesy.in/v1/voice-otp/send with a Bearer API key, so any HTTP client works: cURL, fetch, axios, requests or your framework's client.

### How do I stop people abusing the call button?

Use three layers: Edesy's per-number hourly rate limit, your own per-user and per-IP limits on the button, and a CAPTCHA or device check before the first call on a new device. Never let the button call a number the user hasn't already entered for SMS.

### What if the voice call isn't answered either?

The callback reports no-answer or busy, and the call is not charged. Show the user a clear message, let them retry after a short wait, and point them to support if both channels fail.

### What does the fallback cost?

Rs 1.50 per answered minute with a 1-minute minimum, plus 18% GST. You only pay for voice OTPs that were actually answered, and only on logins where the SMS didn't do the job.
