Backend live · REST complete · Mobile SDK queued

Integrate Zoza Auth

Swap SMS OTP for a 2-second Curve25519 handshake. Register an app, enrol devices, issue challenges, verify cryptographic proofs — REST API live today, mobile SDKs land next.

Quickstart

Five HTTP calls end-to-end. You own steps 1 + 3 + 6; the user's phone owns steps 2 + 4 + 5.

cURLauth-api.zoza.world
# 1. You (bank) register your app — one time, returns your API key
$ curl -X POST https://auth-api.zoza.world/v1/apps \
    -H "Content-Type: application/json" \
    -d '{"name":"HDFC NetBanking","webhook_url":"https://hdfc.example.com/auth/wh"}'
{"id":"app_...", "api_key":"za_...", "webhook_key":"whk_...", "public_key":"<hex>"}

# 2. Each user enrols a device (you forward their device pubkey)
$ curl -X POST https://auth-api.zoza.world/v1/devices \
    -H "Authorization: Bearer za_..." \
    -d '{"user_id":"user_rahul","device_id":"iphone_15_pro","device_name":"iPhone 15 Pro","public_key":"<32-byte hex>"}'

# 3. You issue a challenge when the user logs in / approves a payment
$ curl -X POST https://auth-api.zoza.world/v1/challenges \
    -H "Authorization: Bearer za_..." \
    -d '{"user_id":"user_rahul","context":"payment","metadata":"Pay ₹5000 to Flipkart","ttl":30}'
{"challenge_id":"ch_...", "expires_at":"2026-04-17T14:22:48Z", "status":"pending"}

# 4. Device fetches challenge details (no auth — challenge_id is the secret)
$ curl https://auth-api.zoza.world/v1/challenges/ch_.../details

# 5. Device signs + POSTs the proof (client-side computation, see below)
$ curl -X POST https://auth-api.zoza.world/v1/challenges/ch_.../respond \
    -d '{"device_id":"iphone_15_pro","client_pub":"<hex>","proof":"<hex>","approved":true}'
{"status":"approved", "user_id":"user_rahul", "verified_at":"2026-04-17T14:22:51Z"}

# 6. You poll /v1/challenges/ch_... (or receive the webhook)
$ curl https://auth-api.zoza.world/v1/challenges/ch_... \
    -H "Authorization: Bearer za_..."
{"status":"approved", "used_at":"..."}

That's the whole loop. No SMS. No shared secret on the wire. Private key lives in the Secure Enclave / Android Keystore — our server never sees it.

Pick your integration

Zoza Auth ships one live surface today plus two planned SDKs. Use whichever fits your stack.

🌐
Any language

REST API (live)

HTTP + JSON. Call from Go, Java, Python, Kotlin, Swift. Full reference below. Every endpoint CORS-open.

📩
Server-side

Webhooks

Get pushed approved / denied results within 2s of the user's tap. HMAC-signed with your webhook_key.

📱
Coming soon

iOS SDK

Swift wrapper over Secure Enclave key generation + biometric prompt. ETA 30 dev days post-launch.

🤖
Coming soon

Android SDK

Kotlin wrapper over Android Keystore + biometric prompt. ETA 25 dev days post-launch.

Until SDKs ship:

Your mobile team can use the REST API directly. Curve25519 is available in every platform: CryptoKit.Curve25519 on iOS 14+, java.security.KeyPairGenerator with "XDH" on Android 12+, libsodium everywhere else. Client-side crypto reference ↓ below.

Apply for an API Key

Submit below — we review within 24 hours and email your za_... API key on approval. Keys are shown once at issuance; store yours in a secret manager the moment you receive it.

Authentication

Pass your issued API key as a Bearer token on every customer-facing endpoint:

Authorization: Bearer za_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Device-facing endpoints (challenge fetch + respond) are not bearer-authenticated — the challenge_id is the single-use secret, valid for 30 seconds. This is intentional: the user's device should not have to ship your API key.

Base URL

https://auth-api.zoza.world

Also reachable at https://zoza-products.fly.dev/auth/… (with /auth prefix). The dedicated subdomain is the stable public surface.

REST API reference

Register app

POST/v1/appsFREE
Register a customer app. Returns a one-time api_key, a persistent webhook_key, and your server-side public_key. Save all three — keys are never shown again.

Body: { "name": string, "webhook_url": string? }. Typical response fields:

fieldexamplenotes
idapp_8e41b805128845c2c1a4a9a047380d8f…Identifies your app in challenge metadata + audit log.
api_keyza_f31a8bc9…Authorise requests. Secret; rotate by calling POST /v1/apps again.
webhook_keywhk_cf22…HMAC-key we use to sign outbound webhooks to your server.
public_key<hex 32B>Pin on the device at enrol time. Mutual-auth binds challenges.
rate_limit10Max challenges per user per minute. Adjustable at approve time.

Register device

POST/v1/devicesBEARER
Enrol a user's device. The device generates its own Curve25519 keypair client-side; you forward the public half here.
POST /v1/devices
Authorization: Bearer za_...
Content-Type: application/json

{
  "user_id":    "user_rahul",         # your user identifier
  "device_id":  "iphone_15_pro",      # unique per (user, device)
  "device_name":"iPhone 15 Pro",      # human-readable
  "public_key": "<64 hex chars = 32 bytes>"
}

Revoke device

DELETE/v1/devices/{id}BEARER
Mark a device inactive. Future challenges to that device fail at verify time. Use for lost / stolen phones, account takeover response.

Issue challenge

POST/v1/challengesBEARER
Issue an auth challenge for a user. Push fan-out happens server-side; the user sees it inside your app within ~100ms.
POST /v1/challenges
Authorization: Bearer za_...

{
  "user_id":  "user_rahul",
  "context":  "payment",             # free-form: login, payment, transfer, enrol
  "metadata": "Pay ₹5000 to Flipkart", # shown to user in the biometric prompt
  "ttl": 30                        # seconds, 1-300. Default 30.
}

Response: { "challenge_id":"ch_...", "status":"pending", "expires_at":"...", "ttl_seconds":30 }

Poll challenge status

GET/v1/challenges/{id}BEARER
Long-poll loop for banks that don't use webhooks. Returns current status. Auto-expires stale challenges past TTL.

Fetch challenge (device-side)

GET/v1/challenges/{id}/detailsCHALLENGE-ID
The user's device fetches what to show them. No API key — the challenge_id itself is the single-use secret. Expires after 30s or on use.
GET /v1/challenges/ch_.../details

{
  "challenge_id": "ch_...",
  "context":     "payment",
  "metadata":    "Pay ₹5000 to Flipkart",
  "server_pub":  "<hex 32B>",
  "nonce":       "<hex 32B>",
  "expires_at":  "2026-04-17T14:22:48Z"
}

Respond (device-side)

POST/v1/challenges/{id}/respondCHALLENGE-ID
Device submits the cryptographic proof. Server verifies in constant-time; returns the result synchronously.
POST /v1/challenges/ch_.../respond
Content-Type: application/json

{
  "device_id": "iphone_15_pro",
  "client_pub":"<hex 32B>",
  "proof":    "<hex 32B>",           # HKDF(DH(device_priv, server_pub), nonce)
  "approved": true
}

→ 200
{"status":"approved", "user_id":"user_rahul", "device_id":"iphone_15_pro", "verified_at":"..."}

Audit log

GET/v1/apps/{id}/auditBEARER
Fetch the last 100 audit events for your app. Events: device_registered, device_revoked, challenge_issued, challenge_approved, challenge_denied, challenge_expired. Retained 90 days. Full policy at /about/auth-audit.

Client-side crypto reference

Until our native SDKs ship, your mobile team computes the proof directly with platform crypto. The protocol is: generate a Curve25519 keypair at enrol, retrieve the server's server_pub from /details, compute X25519 shared secret, HKDF-SHA256 with the nonce as salt. 20 lines per platform.

Swift (iOS 14+)
import CryptoKit

// Enrol — generate + store in Secure Enclave
let devicePriv = try SecureEnclave.P256.KeyAgreement.PrivateKey()
let devicePubHex = devicePriv.publicKey.rawRepresentation.map { String(format: "%02x", $0) }.joined()
// POST devicePubHex to /v1/devices via your server

// Respond — compute proof after biometric auth
func computeProof(serverPub: [UInt8], nonce: [UInt8]) throws -> Data {
    let server = try P256.KeyAgreement.PublicKey(rawRepresentation: Data(serverPub))
    let shared = try devicePriv.sharedSecretFromKeyAgreement(with: server)
    let proof  = shared.hkdfDerivedSymmetricKey(
        using: SHA256.self,
        salt: Data(nonce),
        sharedInfo: "ZozaAuth_Proof_v1".data(using: .utf8)!,
        outputByteCount: 32)
    return proof.withUnsafeBytes { Data($0) }
}
Kotlin (Android 12+)
import java.security.KeyPairGenerator
import javax.crypto.KeyAgreement
import javax.crypto.spec.SecretKeySpec
import javax.crypto.Mac

// Enrol — Android Keystore-backed XDH (X25519) keypair
val kpg = KeyPairGenerator.getInstance("XDH", "AndroidKeyStore")
kpg.initialize(KeyGenParameterSpec.Builder("zoza_auth_device", PURPOSE_AGREE_KEY)
    .setAlgorithmParameterSpec(ECGenParameterSpec("X25519"))
    .setUserAuthenticationRequired(true)     // biometric gate
    .build())
val kp = kpg.generateKeyPair()

// Respond — X25519 + HKDF-SHA256 with nonce as salt
val ka = KeyAgreement.getInstance("XDH", "AndroidKeyStore")
ka.init(kp.private)
ka.doPhase(serverPubKey, true)
val shared: ByteArray = ka.generateSecret()
val proof = HkdfSha256.derive(shared, nonce, "ZozaAuth_Proof_v1".toByteArray(), 32)

Reference implementation in Go lives at products/zoza-auth/auth.go:537computeAuthProof. If your platform crypto doesn't agree with our Go output bit-for-bit, email us with your test vectors and we'll find the disagreement.

Webhooks

Instead of polling GET /v1/challenges/{id}, set webhook_url when you register. We POST the AuthResult JSON to you within ~2s of the user's tap, HMAC-signed with your webhook_key.

POST https://your-bank.example.com/auth/webhook
X-Zoza-Signature: sha256=<hex HMAC of body>

{
  "challenge_id": "ch_...",
  "app_id":       "app_...",
  "user_id":      "user_rahul",
  "device_id":    "iphone_15_pro",
  "device_name":  "iPhone 15 Pro",
  "status":       "approved",
  "context":      "payment",
  "verified_at":  "2026-04-17T14:22:51Z"
}
Webhook replay (today)

Webhook body carries an HMAC signature over the JSON. An incrementing nonce field is on the roadmap (ETA ~3 dev days). Until then, dedupe idempotently on challenge_id — every challenge is one-shot.

Error codes

HTTPMeaningWhen
200OKStatus fetch / respond / audit read.
201CreatedApp, device, or challenge created.
400Bad RequestMalformed body, bad hex key, bad TTL, missing user_id.
401UnauthorizedMissing / wrong Bearer token.
403ForbiddenVerify-step failed: public-key mismatch, wrong device, invalid DH proof.
404Not FoundUnknown app, device, or challenge ID.
409ConflictChallenge already approved / denied / expired; reapprove rejected.
410GoneChallenge TTL elapsed before respond.
429Too Many RequestsRate limit: per-user challenges/min, or per-email apply submissions.
500Server ErrorBug on our side — email hello@zoza.world with the request ID.

Plans & rate limits

Free

$0
1,000 auths/day · 10 challenges/user/min
  • REST API + webhooks
  • 1 registered app
  • Community support

Enterprise

Custom
10M+ auths/day
  • Everything in Pro
  • SLA + on-call
  • Dedicated HSM / threshold server key
  • On-prem / VPC option
  • Custom compliance (RBI, HIPAA, GDPR)

Going-live checklist

Support

Emailhello@zoza.world — integration, key requests, enterprise pricing
Securitysecurity@zoza.world — bug bounty submissions (scope + payouts)
Statusauth-api.zoza.world/health — live uptime
AuditAudit policy · Canary · Retention · Bounty
Twitter@zoza_world — launches, incidents, roadmap
SourceSource-available. Mobile SDKs + threshold server key in flight. Integration today is via REST.

© Zoza · zoza.world · Auth is source-available; mobile SDKs + threshold key ship post-launch.