Server-side

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_yyy

2. 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 requestId

Never 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 (at setup, 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.

Chat with us