Backend integration
QuickAuth is a verification provider, not an identity provider — we tell you whether the phone was verified, you own the user table and the session. Your backend has two jobs: (1) mint short-lived SDK session tokens so the SDK can talk to us, and (2) confirm OTP verifications server-to-server before you mint your own session JWT.
The shape
┌────────────┐ /api/quickauth-token ┌──────────────────┐ POST /v1/sdk/session │ Your app │ ──────────────────────▶ │ Your backend │ ──────────────────────▶ QuickAuth │ (this SDK) │ ◀────────────────────── │ (client_secret) │ ◀────────────────────── (sessionToken, exp 10m) └────────────┘ sessionToken └──────────────────┘
Never ship client_secret to the device
Anyone can extract a string from an iOS/Android bundle or a JS app. The SDKs deliberately have no parameter for qa_secret_***. Keep it on your server.
1. Set environment variables
Two values, both off the dashboard API keys page:
QUICKAUTH_CLIENT_ID=qa_client_xxx
QUICKAUTH_CLIENT_SECRET=qa_secret_yyy2. Expose /api/quickauth-token
The endpoint authenticates the request from your app (your existing session/JWT auth), then proxies a server-to-server call to QuickAuth.
import express from 'express'
const app = express()
app.post('/api/quickauth-token', requireAuth, async (req, res) => {
const r = await fetch('https://api.quickauth.in/v1/sdk/session', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Client-Id': process.env.QUICKAUTH_CLIENT_ID,
'X-Client-Secret': process.env.QUICKAUTH_CLIENT_SECRET,
},
body: JSON.stringify({ scope: 'sdk' }),
})
if (!r.ok) return res.status(502).json({ error: 'upstream' })
res.json(await r.json())
})3. The SDK does the rest
On every QuickAuth API call, the SDK checks its in-memory cache for a non-expired sessionToken. If missing or within 30 s of expiry, it calls your onTokenExpiry callback (which calls /api/quickauth-token) and uses the fresh token.
Concurrent callers share a single refresh — no thundering herd. On HTTP 401, the SDK invalidates the cached token and retries the request exactly once with a freshly-minted one.
Responsibility 2 — confirm verifications and mint your own session
When verifyOTP returns { verified: true, requestId, message } inside the app, you should not trust that response on its own — a malicious client could fabricate it. Your app forwards the requestId to your backend; your backend confirms with QuickAuth server-to-server, then mints your own session JWT against your own user table.
The shape
SDK in app
│ POST /v1/sdk/auth/verify
▼
QuickAuth → { verified: true, requestId: "req_8h3k…", message: "Verified successfully" }
│
│ app forwards { requestId } to its own backend
▼
Your backend
│ GET /v1/auth/status?requestId=req_8h3k…
│ X-Client-Id / X-Client-Secret
▼
QuickAuth → { authStatus: VERIFIED, identity: "+91…", … }
│
│ look up / create user by phone
│ mint YOUR session JWT
▼
Your app → app stores your JWT, drops the QuickAuth requestIdNever trust the verify response from the app
The { verified: true } coming back to the SDK is informational — the SDK uses it to advance UI state. Anything that grants access on your sidemust be gated by the server-to-server confirmation below.
Confirm + mint your own session
// POST /api/login/phone-verified
app.post('/api/login/phone-verified', async (req, res) => {
const { requestId } = req.body
if (!requestId) return res.status(400).json({ error: 'requestId required' })
// 1. Server-to-server confirmation with QuickAuth
const r = await fetch(`https://api.quickauth.in/v1/auth/status?requestId=${requestId}`, {
headers: {
'X-Client-Id': process.env.QUICKAUTH_CLIENT_ID,
'X-Client-Secret': process.env.QUICKAUTH_CLIENT_SECRET,
},
})
const status = await r.json()
if (status.authStatus !== 'VERIFIED') {
return res.status(401).json({ error: 'not verified' })
}
// 2. Look up / create the user in YOUR table
let user = await db.users.findOne({ phone: status.identity })
if (!user) user = await db.users.create({ phone: status.identity })
// 3. Mint YOUR session JWT — your format, your expiry, your claims
const sessionJWT = jwt.sign(
{ userId: user.id, role: user.role },
process.env.SESSION_SECRET,
{ expiresIn: '7d' },
)
res.json({ sessionJWT })
})Why this pattern
QuickAuth never sees your user table, your sessions, your role / permission model. If you later move off QuickAuth, your users stay logged in — only the next phone verification has to be re-done. Compare to identity providers (Clerk, Auth0) where every active session is tied to the provider.
Pre-warming (optional)
If you already fetched a token (during onboarding, say), hand it to the SDK so the first API call doesn’t have to await the network:
QuickAuth.shared.initialize(
publicKey: "qa_live_***",
initialToken: cachedToken,
onTokenExpiry: { try await myAPI.fetch("/api/quickauth-token").sessionToken }
)Trusted-enterprise escape hatch
Not recommended for public-distribution apps
For internal-only apps (MDM, kiosk, field-ops, server-to-server) where the binary lives in a trusted environment, the SDKs accept unsafeDirectClientId + unsafeDirectClientSecret and mint their own tokens. The SDK prints a loud warning on init. Never use this for App Store / Play Store apps.
Webhooks
QuickAuth posts events to your registered webhook for verification state changes (otp.verified, otp.expired, session.created), campaign deliveries, and refunds. See the REST API reference for webhook signing.