Server-side · v1

REST API reference

Every QuickAuth feature is reachable over plain HTTPS — handy when you’re on an unsupported stack, doing server-to-server orchestration, or just want to curl-test before you wire the SDK.

Base URL

https://api.quickauth.in

All endpoints accept and return application/json.

Authentication

Three header schemes depending on which surface you’re hitting:

HeaderUsed forKey format
X-Public-KeySDK endpoints (/v1/sdk/*) — safe in clientsqa_live_*** / qa_test_***
X-Client-Id + X-Client-SecretServer-to-server (token mint, dashboard ops)qa_client_*** + qa_secret_***
Authorization: Bearer …SDK calls authenticated with sessionToken10-min JWT

Conventions

  • All timestamps are ISO 8601 UTC.
  • Error responses have shape { errorCode, message } with the matching HTTP status — errorCode is an UPPER_SNAKE constant (see Error codes).
  • POST /v1/auth/initiate is rate-limited per identity per hour (default 10/hour, configurable per merchant). Over the limit returns 429 RATE_LIMIT_EXCEEDED.

Direct OTP API (server-to-server)

The raw server endpoints — authenticate with X-Client-Id + X-Client-Secret. Use these when you send and verify OTPs entirely from your own backend (no SDK). This is the pair behind a typical curl integration.

POST/v1/auth/initiate

Send an OTP

Generates and dispatches the OTP over the chosen channel and returns a requestId you pass to /verify.

curl https://api.quickauth.in/v1/auth/initiate \
  -H "Content-Type: application/json" \
  -H "X-Client-Id: qc_xxx" \
  -H "X-Client-Secret: qa_secret_yyy" \
  -d '{
    "identity":     "+919876543210",
    "identityType": "MOBILE",
    "mode":         "OTP",
    "channel":      "WHATSAPP"
  }'

Responses

  • 200Accepted by the provider. The OTP is on its way.
  • 400A field is missing or malformed.INVALID_REQUEST
  • 401Client id or secret is wrong.INVALID_CLIENT_CREDENTIALS
  • 401The account is suspended.CLIENT_INACTIVE
  • 402Out of credit for this channel.INSUFFICIENT_BALANCE
  • 429Too many initiates for this identity this hour.RATE_LIMIT_EXCEEDED
  • 502The SMS or WhatsApp provider refused the send.PROVIDER_ERROR

Allowed values

FieldAccepted valuesNotes
modeOTP, OAUTH, MAGIC_LINKUse OTP for send-and-verify OTP.
identityTypeMOBILE, EMAILMOBILE = phone number, EMAIL = email address.
channelSMS, WHATSAPP, RCS, EMAILOptional — omit to let the router pick. RCS and EMAIL are live channels, enabled on your account during onboarding.

Valid combinations

Not every combination works: phone OTP is identityType: "MOBILE" with channel: "SMS" or "WHATSAPP"; email OTP is identityType: "EMAIL" with channel: "EMAIL". Keep mode: "OTP" for the send/verify flow — OAUTH and TRUECALLER belong to a different (identity-token) flow.

POST/v1/auth/verify

Verify the OTP

Checks the user-entered code against the requestId. Returns verified: true/false.

curl https://api.quickauth.in/v1/auth/verify \
  -H "Content-Type: application/json" \
  -H "X-Client-Id: qc_xxx" \
  -H "X-Client-Secret: qa_secret_yyy" \
  -d '{
    "requestId": "req-abc123",
    "otp":       "123456"
  }'

Responses

  • 200Checked. Read verified — true and false both arrive as a 200.
  • 400The code expired; call /initiate again.OTP_EXPIRED
  • 400Too many wrong codes for this requestId.MAX_ATTEMPTS_EXCEEDED
  • 400No transaction matches that requestId or orderId.TRANSACTION_NOT_FOUND
  • 401Client id or secret is wrong.INVALID_CLIENT_CREDENTIALS

A wrong code is a 200, not a 4xx

Branch on the verified field, never on the status code. An incorrect OTP is a successful API call that answers “no” — the request was well-formed and we checked it. Treating any non-2xx as “wrong code” silently turns an expired transaction, a revoked credential and a wrong code into the same message for the user, and only one of those is fixed by typing more carefully.

requestId vs orderId

Pass either requestId (returned by /initiate) or the orderId you supplied at initiate — not both.otp is the code the user received.

Session

POST/v1/sdk/session

Mint a 10-minute session token

Called by your backend so SDKs can authenticate. Requires server credentials.

curl https://api.quickauth.in/v1/sdk/session \
  -H "Content-Type: application/json" \
  -H "X-Client-Id: qa_client_xxx" \
  -H "X-Client-Secret: qa_secret_yyy" \
  -d '{"scope":"sdk"}'

OTP

POST/v1/sdk/auth/initiate

Start an OTP verification

Returns a sessionId. The SDK uses this to verify the user-entered code.

POST /v1/sdk/auth/initiate
Content-Type: application/json
X-Public-Key: qa_live_***
Idempotency-Key: <uuid>

{
  "phone":      "+919876543210",
  "channel":    "auto",            // "auto" | "sms" | "whatsapp"
  "template":   "login_otp",       // optional, defaults to your default template
  "deviceInfo": {                  // optional, used for trusted-device / OneTap
    "platform":  "ios",
    "model":     "iPhone 14",
    "osVersion": "18.2",
    "appVersion":"3.4.1",
    "locale":    "en-IN"
  }
}
POST/v1/sdk/auth/verify

Verify the OTP code

Returns verified: true/false and a requestId. QuickAuth deliberately does not issue a session token — the merchant's backend confirms the verification server-to-server and mints its own session JWT. See Backend integration.

POST /v1/sdk/auth/verify
Content-Type: application/json
Authorization: Bearer <sdk-session-token>
Idempotency-Key: <uuid>

{
  "sessionId": "qas_5fA2…",
  "code":      "123456"
}

Why no JWT here

QuickAuth is a verification provider, not an identity provider. We tell you whether the phone was verified — your backend owns the user table and the session. The merchant's app forwards the requestId to its own backend, which confirms the result via GET /v1/auth/status (below) and then mints its own session JWT against its own user table.

GET/v1/auth/status

Confirm a verification (server-to-server)

Your backend calls this with the requestId the app forwarded. The response reveals the authoritative auth status. Use server credentials — never call this from the app.

curl 'https://api.quickauth.in/v1/auth/status?requestId=req_8h3k…' \
  -H 'X-Client-Id: qa_client_***' \
  -H 'X-Client-Secret: qa_secret_***'

Responses

  • 200The transaction was found. Read authStatus.
  • 400No transaction matches that requestId or orderId.TRANSACTION_NOT_FOUND
  • 401Client id or secret is wrong.INVALID_CLIENT_CREDENTIALS

authStatus values

ValueMeaning
PENDINGSent, not yet verified, not yet expired.
VERIFIEDThe user entered the correct code. verifiedAt is set.
EXPIREDThe TTL elapsed with no successful verify.
FAILEDAttempts were exhausted, or the send could not be delivered at all.

deliveries carries one entry per send attempt, so a fallback shows up as two rows against the same requestId — the WhatsApp attempt and the SMS that followed it. Each entry’s deliveryStatus is one of PENDING, SENT, DELIVERED, READ, FAILED or SKIPPED.

Sending the OTP over WhatsApp

There is no separate WhatsApp endpoint. WhatsApp is a channel on the same /v1/auth/initiate call — the request body, the requestId you get back, /v1/auth/verify and /v1/auth/status are identical whichever channel carried the code. Switching channel is a one-field change, not an integration.

curl -X POST 'https://api.quickauth.in/v1/auth/initiate' \
  -H 'Content-Type: application/json' \
  -H 'X-Client-Id: qa_client_***' \
  -H 'X-Client-Secret: qa_secret_***' \
  -d '{
    "identity": "+919876543210",
    "identityType": "MOBILE",
    "mode": "OTP",
    "channel": "WHATSAPP"
  }'

What actually differs by channel

Only what it takes to send: WhatsApp needs an approved template on your WABA, SMS needs a DLT-registered one. Everything after the send — verification, attempt counting, expiry, status — is one code path. If you have configured a fallback, an undelivered WhatsApp message re-sends the same code over SMS against the same requestId.

Attribution

POST/v1/sdk/attribution/launch

Record a launch event

Captures UTMs, qa_clid, install referrer, fingerprint. Consent-gated.

POST/v1/sdk/attribution/conversion

Record a conversion

Free-form event with optional value and currency.

Webhooks

Configure a webhook URL on the dashboard. Events you’ll receive:

EventFired when
otp.initiatedAn OTP verification is started.
otp.verifiedThe user entered the correct code.
otp.expiredThe OTP timed out without verification.
session.createdA 10-minute SDK session was minted.
delivery.failedSMS/WhatsApp dispatch failed permanently.
campaign.deliveredMarketing campaign batch finished.

Signing

Every webhook carries an X-QuickAuth-Signature header — HMAC-SHA256 of the raw body with your webhook secret. Verify it before trusting the payload.

Errors

Every failure returns the same envelope, whichever endpoint produced it. The errorCode is the stable part — branch on it, not on the message, which is written for a human reading a log and may be reworded.

error envelope
{
  "errorCode": "RATE_LIMIT_EXCEEDED",
  "message":   "Rate limit exceeded. Try again later.",
  "requestId": "req_8h3k…"
}

requestId is present when the failure happened against a known transaction and is null otherwise — quote it to support and we can find the exact send.

HTTP status codes

StatusWhat it meansRetry?
200Handled. For /verify this includes a wrong code — read verified.—
400Your request. A field is missing, malformed, or the transaction is expired or out of attempts.Not until you change it
401Credentials rejected, or the account is inactive.No
402Out of credit for the channel you asked for.After a top-up
403Authenticated, but not permitted to touch this resource.No
404No such template, webhook, or merchant.No
409Conflicts with something that already exists, or with the resource’s current state.No
413Upload exceeded the size cap.With a smaller file
429Rate limited for this identity.Yes, with backoff
500Our fault. Nothing about the request needs changing.Yes, with backoff
502The downstream SMS or WhatsApp provider failed.Yes, with backoff

Which failures are worth retrying

429, 500 and 502 are transient — retry with exponential backoff. Everything in the 400–409 range will fail again identically until something changes on your side, so a retry loop there just burns rate limit. When you retry a send, carry the same idempotencyKey so a request that actually succeeded before the timeout does not go out twice.

Error codes

errorCodeHTTPMeaning
INVALID_REQUEST400Body failed validation — the message names the field.
INVALID_MODE400mode is not one of OTP, OAUTH, MAGIC_LINK.
INVALID_CHANNEL400channel is not valid for this identity type.
CHANNEL_REQUIRED400OAUTH needs an explicit channel.
REDIRECT_URI_REQUIRED400OAUTH needs a redirectUri.
OTP_EXPIRED400TTL elapsed — call /initiate again.
MAX_ATTEMPTS_EXCEEDED400Too many wrong codes for this requestId.
TRANSACTION_NOT_FOUND400No transaction for that requestId / orderId.
TEMPLATE_NOT_FOUND400 / 404No template configured for the requested send.
INVALID_TEMPLATE_CONFIG400The template exists but is not usable as configured.
INCOMPLETE_TEMPLATE_CONFIG400The template is missing something it needs to send.
CSV_VALIDATION_FAILED400An uploaded recipient list did not parse.
INVALID_CLIENT_CREDENTIALS401X-Client-Id / X-Client-Secret missing or wrong.
CLIENT_INACTIVE401The account is suspended.
INSUFFICIENT_BALANCE402Out of credit.
INSUFFICIENT_SMS_BALANCE402Out of SMS credit specifically.
INSUFFICIENT_WHATSAPP_BALANCE402Out of WhatsApp credit specifically.
BILLING_NOT_CONFIGURED402No billing set up for the account yet.
FORBIDDEN403Authenticated, but not yours to touch.
MERCHANT_NOT_FOUND404No such merchant.
WEBHOOK_NOT_FOUND404No webhook with that id.
DUPLICATE_TEMPLATE409A template with that name already exists.
DUPLICATE_SENDER_ID409That sender ID is already registered.
WEBHOOK_URL_DUPLICATE409That webhook URL is already registered.
RATE_LIMIT_EXCEEDED429Too many /initiate calls for this identity this hour.
INTERNAL_ERROR500Our fault. Safe to retry.
PROVIDER_ERROR502Downstream SMS / WhatsApp provider refused the send.
WHATSAPP_CONNECT_FAILED502Could not reach WhatsApp to complete the connection.

SDK quickstart from API

If you’ve worked through this page directly, the SDK pages give you the same calls wrapped with native idioms, auto-fill helpers, and consent plumbing. Pick a stack from the docs home.

Chat with us