Prerequisites
To use OTP services you need:- A workspace with
developer_access = true. Toggle this in the Briq UI under workspace settings, or see Workspaces. - A Developer App linked to that workspace. Each app gives you an
app_key(sent in every OTP request body) and anapp_id(optionalX-App-IDheader). See Developer Apps. - An active developer API key (
X-API-Keyheader). Generate one in the Briq dashboard. - Recipient phone numbers in E.164 digits-only format (no
+, no spaces) — e.g.255712345678.
The
app_key is a credential. Hold it server-side; never ship it to untrusted clients. Have your frontend call your backend, which then calls Karibu.Behavioral rules to internalize
These rules drive correct UX and prevent the most common integration bugs. Read them once before writing code.| Rule | Detail |
|---|---|
Single active OTP per (phone, app) | request and resend invalidate any prior unused, unexpired OTP for the same phone + app before issuing a new one. There is never more than one active OTP for a phone within an app. |
| Hashed at rest | OTP codes are bcrypt-hashed in the database. The plaintext is delivered only via SMS/call to the recipient. The server will never return it in any response. |
3-attempt cap on verify | After 3 wrong codes the OTP is auto-locked (marked used) and remaining_attempts becomes 0. The user must request a new OTP. |
| Phone format | E.164 digits only, no +. Invalid input → 400 "Invalid phone number". |
| Cross-app isolation | Verify, resend, invalidate, and status all operate only on OTPs issued under the same Developer App. An OTP issued under app A cannot be touched via app B. |
| Default expiry | 10 minutes. Configure per call via minutes_to_expire. |
| Default OTP length | 6 digits. Configure per call via otp_length. |
| Delivery channel | "sms" (default), "call", or "whatsapp". Each has its own payload shape — see the per-channel tabs in Requesting OTP codes. |
| Channel-specific fields | sender_id and message_template are SMS-only — silently ignored on "call" and "whatsapp". Voice reads the code via TTS; WhatsApp uses the approved briq_otp template. |
End-to-end flow
A typical phone-verification flow:- User enters phone in your UI → your backend calls
POST /v1/otp/requestwith the appropriatedelivery_method. → see Requesting OTP codes. - User receives SMS, voice call, or WhatsApp message with the plaintext code.
- User submits code in your UI → your backend calls
POST /v1/otp/verify. → see Validating OTP codes.- On
success: true→ mark phone verified and continue. - On
success: false→ drive the UX offdata.remaining_attempts.
- On
- User clicks “Resend” → your backend calls
POST /v1/otp/resend(which invalidates the prior code automatically). You may switch channels here — e.g. retry on WhatsApp after an SMS didn’t arrive. → see Managing OTP lifecycle. - User logs out / changes phone → your backend calls
POST /v1/otp/invalidate. - Optional: poll
GET /v1/otp/statusto drive countdown timers or detect that an OTP is already in-flight before issuing a new one.
Standard response envelope
Every OTP endpoint returns the same JSON envelope:body.success and body.status_code rather than the raw HTTP status — /v1/otp/status returns HTTP 200 wrapping status_code: 404 when no active OTP exists.
Auth, host, and validation errors (401/403/422) follow FastAPI’s {"detail": "..."} shape instead — they bypass the envelope.
For complete payload tables, error matrices, and copy-pasteable client snippets in cURL, Python, Node.js, and PHP, see the Karibu OTP API reference.
Need to send WhatsApp messages beyond OTP, such as notifications, templates, media, or interactive flows? See the WhatsApp guide.