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:
| Header | Used for | Key format |
|---|---|---|
X-Public-Key | SDK endpoints (/v1/sdk/*) — safe in clients | qa_live_*** / qa_test_*** |
X-Client-Id + X-Client-Secret | Server-to-server (token mint, dashboard ops) | qa_client_*** + qa_secret_*** |
Authorization: Bearer … | SDK calls authenticated with sessionToken | 10-min JWT |
Conventions
- All timestamps are ISO 8601 UTC.
- Error responses have shape
{ errorCode, message }with the matching HTTP status —errorCodeis an UPPER_SNAKE constant (see Error codes). POST /v1/auth/initiateis rate-limited per identity per hour (default 10/hour, configurable per merchant). Over the limit returns429 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.
/v1/auth/initiateSend 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
| Field | Accepted values | Notes |
|---|---|---|
mode | OTP, OAUTH, MAGIC_LINK | Use OTP for send-and-verify OTP. |
identityType | MOBILE, EMAIL | MOBILE = phone number, EMAIL = email address. |
channel | SMS, WHATSAPP, RCS, EMAIL | Optional — 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.
/v1/auth/verifyVerify 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
/v1/sdk/sessionMint 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
/v1/sdk/auth/initiateStart 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"
}
}/v1/sdk/auth/verifyVerify 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.
/v1/auth/statusConfirm 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
| Value | Meaning |
|---|---|
PENDING | Sent, not yet verified, not yet expired. |
VERIFIED | The user entered the correct code. verifiedAt is set. |
EXPIRED | The TTL elapsed with no successful verify. |
FAILED | Attempts 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
/v1/sdk/attribution/launchRecord a launch event
Captures UTMs, qa_clid, install referrer, fingerprint. Consent-gated.
/v1/sdk/attribution/conversionRecord a conversion
Free-form event with optional value and currency.
Webhooks
Configure a webhook URL on the dashboard. Events you’ll receive:
| Event | Fired when |
|---|---|
otp.initiated | An OTP verification is started. |
otp.verified | The user entered the correct code. |
otp.expired | The OTP timed out without verification. |
session.created | A 10-minute SDK session was minted. |
delivery.failed | SMS/WhatsApp dispatch failed permanently. |
campaign.delivered | Marketing 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.
{
"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
| Status | What it means | Retry? |
|---|---|---|
200 | Handled. For /verify this includes a wrong code — read verified. | — |
400 | Your request. A field is missing, malformed, or the transaction is expired or out of attempts. | Not until you change it |
401 | Credentials rejected, or the account is inactive. | No |
402 | Out of credit for the channel you asked for. | After a top-up |
403 | Authenticated, but not permitted to touch this resource. | No |
404 | No such template, webhook, or merchant. | No |
409 | Conflicts with something that already exists, or with the resource’s current state. | No |
413 | Upload exceeded the size cap. | With a smaller file |
429 | Rate limited for this identity. | Yes, with backoff |
500 | Our fault. Nothing about the request needs changing. | Yes, with backoff |
502 | The 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
| errorCode | HTTP | Meaning |
|---|---|---|
INVALID_REQUEST | 400 | Body failed validation — the message names the field. |
INVALID_MODE | 400 | mode is not one of OTP, OAUTH, MAGIC_LINK. |
INVALID_CHANNEL | 400 | channel is not valid for this identity type. |
CHANNEL_REQUIRED | 400 | OAUTH needs an explicit channel. |
REDIRECT_URI_REQUIRED | 400 | OAUTH needs a redirectUri. |
OTP_EXPIRED | 400 | TTL elapsed — call /initiate again. |
MAX_ATTEMPTS_EXCEEDED | 400 | Too many wrong codes for this requestId. |
TRANSACTION_NOT_FOUND | 400 | No transaction for that requestId / orderId. |
TEMPLATE_NOT_FOUND | 400 / 404 | No template configured for the requested send. |
INVALID_TEMPLATE_CONFIG | 400 | The template exists but is not usable as configured. |
INCOMPLETE_TEMPLATE_CONFIG | 400 | The template is missing something it needs to send. |
CSV_VALIDATION_FAILED | 400 | An uploaded recipient list did not parse. |
INVALID_CLIENT_CREDENTIALS | 401 | X-Client-Id / X-Client-Secret missing or wrong. |
CLIENT_INACTIVE | 401 | The account is suspended. |
INSUFFICIENT_BALANCE | 402 | Out of credit. |
INSUFFICIENT_SMS_BALANCE | 402 | Out of SMS credit specifically. |
INSUFFICIENT_WHATSAPP_BALANCE | 402 | Out of WhatsApp credit specifically. |
BILLING_NOT_CONFIGURED | 402 | No billing set up for the account yet. |
FORBIDDEN | 403 | Authenticated, but not yours to touch. |
MERCHANT_NOT_FOUND | 404 | No such merchant. |
WEBHOOK_NOT_FOUND | 404 | No webhook with that id. |
DUPLICATE_TEMPLATE | 409 | A template with that name already exists. |
DUPLICATE_SENDER_ID | 409 | That sender ID is already registered. |
WEBHOOK_URL_DUPLICATE | 409 | That webhook URL is already registered. |
RATE_LIMIT_EXCEEDED | 429 | Too many /initiate calls for this identity this hour. |
INTERNAL_ERROR | 500 | Our fault. Safe to retry. |
PROVIDER_ERROR | 502 | Downstream SMS / WhatsApp provider refused the send. |
WHATSAPP_CONNECT_FAILED | 502 | Could 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.