Pick the login you want

Every flow below ends with a verified phone number. They differ in how much the user has to do to get there — and in what you have to set up first. Start from the experience you want; the reference pages have the parameters.

The number of taps is the product

A login that takes four taps and one that takes none use the same API. What changes is which of them you have configured — a template, an app hash, a fallback channel. Each flow lists what it needs, so you can tell before you build whether you can have it.

My returning users shouldn't have to type a code every single time.

They don't. After a user verifies once, the SDK keeps a device token; the next login presents it and the server verifies them without sending anything. No message, no code, no wait — and no cost to you for a message never sent.

onetap_enabled✓ SDK ≥ 1.1✓ no template needed
OneTap — a device that has verified before0 taps · ~200ms
  1. UserOpens app
  2. Systeminitiate()device token attached
  3. SystemToken matchedtrust still live
  4. UserSigned in
Never fires on a first login — there is no token yet, so the user gets one ordinary OTP before this can apply. It is off by default: set onetap_enabled for the merchant, or every login takes the full path and nothing says why.

I want the OTP to fill itself in, not make the user switch apps to read it.

Send the code over WhatsApp with a zero-tap template. WhatsApp hands it straight to your app and the SDK fills the field. The user never leaves your screen and never sees the message.

approved ZERO_TAP templatepackage + 11-char app hashAndroid only
WhatsApp zero-tap — the code arrives already entered1 tap · ~2s
  1. UserTaps Continue
  2. SystemHandshakeapp → WhatsApp
  3. SystemTemplate sentcode + supported_apps
  4. SystemBroadcastWhatsApp → your app
  5. UserVerified
The handshake is the step nobody expects. WhatsApp will not broadcast a code to an app that did not announce itself first — and without it the message still arrives and simply never auto-fills, with nothing to explain it. The SDK sends it for you inside initiate(). The app hash must be the hash of the build being tested: a debug build is not signed with your release key.
// One call. The code arrives over WhatsApp, the SDK reads it,
// and it verifies without the user typing anything.
await QuickAuth.auth.initiate(
  phone: '+919876543210',
  channel: OtpChannel.whatsapp,
  autoSubmit: true,
);

// Resend later, to the same number — no arguments, so a
// reformatted one cannot start a second transaction.
await QuickAuth.auth.resendOtp();

WhatsApp is cheaper, but not everyone has it. I don't want to pick one.

Set WhatsApp as your primary channel and SMS as fallback. Every login tries WhatsApp; if the message is not delivered within your window, the same code goes out over SMS. The user gets one code either way.

primary + fallback channelDLT template for SMS
WhatsApp first, SMS if it doesn't landsame code · one charge
  1. UserEnters number
  2. SystemWhatsApp sent
  3. SystemNo receiptwindow elapsed
  4. SystemSMS sentsame code
  5. UserEnters code
The fallback re-sends the same code, not a new one — two different codes would leave the user choosing, and they would choose the one that arrived last. Send channel: "auto" and the merchant's configured primary decides, so changing it in the dashboard does not require an app release.

DLT gives me {#alp#} three times. How is my backend supposed to know which is which?

Name them once in the dashboard, then send values by name instead of position. DLT registers what a placeholder allows, never what it means — so the meaning is yours to declare, and it lives with the template rather than in every caller.

DLT-registered templatenames set in dashboard
Naming a DLT template's variablesedit-safe
  1. UserImports templateHi {#alp#}, order {#alp#}
  2. UserNames each slotname · orderId
  3. SystemSend by namevariableValues
  4. SystemFilled in order
Positionally, editing the template silently changes what your existing calls mean — swap two placeholders and customers get the order id where their name should be, with nothing to reject it. Naming moves that knowledge into the template. Renaming later creates a new version and the old names keep working, so an integration already sending is not broken by an edit in a dashboard.
curl -X POST 'https://api.quickauth.in/v1/sms/send' \
  -H 'Content-Type: application/json' \
  -H 'X-Client-Id: YOUR_CLIENT_ID' \
  -H 'X-Client-Secret: YOUR_CLIENT_SECRET' \
  -d '{
    "templateId": "1707169123456789012",
    "to": "+919876543210",
    "senderId": "ELEFNT",
    "idempotencyKey": "order-ORD-1-confirm",
    "variableValues": {
      "name": "Rohit",
      "orderId": "ORD-1",
      "deliveryDate": "Friday"
    }
  }'

Set idempotencyKey from the event, not at random

A retry carrying the same key returns the first result instead of sending again — which is what makes a network timeout safe to retry. Derive it from what happened (order-ORD-1-confirm), not from a fresh UUID per attempt: a new key every time is no protection at all.

Which flow costs what

Every flow above bills the same way — per delivered message, at the rate configured for that channel. OneTap is charged as an authentication on the channel it replaced: it sends nothing, but it is the auth event you are paying for, not the message.

Start here if you are new

Quickstart gets a verified number in about ten minutes with no template work — plain SMS OTP, one SDK call. Come back to this page when you want to take taps out of it.
Chat with us