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