Use POST /v1/otp/request to generate a fresh numeric OTP and deliver it to a phone via SMS (default), voice call, or WhatsApp. The endpoint is scoped to a Developer App you own — every request must include your app_key. Calling it always invalidates any prior unused, unexpired OTP for the same (phone, app) before issuing the new one, so there is never more than one active OTP for a phone within an app.
Each channel has its own payload shape. The fields sender_id and message_template are SMS-only — they exist for SMS branding/wording control and are silently ignored on "call" and "whatsapp". Voice reads the code via TTS; WhatsApp routes through the platform-managed briq_otp template with automatic sender resolution.
Default to "sms" for the broadest reach. Use "call" for SMS-restricted regions or accessibility needs, and "whatsapp" for app-installed users with chat-first habits.
Endpoint
POST https://karibu.briq.tz/v1/otp/request
Headers: X-API-Key (required), Content-Type: application/json (required), X-App-ID (optional — must match the API key’s bound app if both are scoped).
Common request fields
These fields are the same on every channel:
| Field | Type | Required | Default | Notes |
|---|
phone_number | string (E.164 digits) | yes | — | Digits only, no +. e.g. 255712345678. |
app_key | string | yes | — | From your Developer App. |
delivery_method | string | yes (recommended) | "sms" | One of "sms", "call", "whatsapp". |
otp_length | int | no | 6 | Length of the generated code. |
minutes_to_expire | int | no | 10 | TTL in minutes. |
Success response
The same shape on every channel:
{
"success": true,
"message": "OTP Code sent successfully.",
"data": { "expires_at": "2026-05-08T12:34:56.000000" },
"status_code": 200
}
The plaintext code is never returned. The recipient receives it via SMS, voice call, or WhatsApp only. Don’t try to capture or log it on the server.
Channel-specific payload & code samples
Default channel. Customisable per call via sender_id and message_template.Channel-specific fields:| Field | Type | Required | Default | Notes |
|---|
sender_id | string | no | OTP_DEFAULT_SENDER_ID | The SMS sender ID shown to the recipient. Must be approved for your account. |
message_template | string | no | server default | Must contain {code}. {expiry} is also substituted. Falls back if {code} missing. |
Exact payload:{
"phone_number": "255712345678",
"app_key": "YOUR_APP_KEY",
"delivery_method": "sms",
"otp_length": 6,
"minutes_to_expire": 10,
"sender_id": "BRIQ OTP",
"message_template": "Your verification code is {code}. It expires in {expiry} minutes."
}
curl -X POST https://karibu.briq.tz/v1/otp/request \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone_number": "255712345678",
"app_key": "YOUR_APP_KEY",
"delivery_method": "sms",
"otp_length": 6,
"minutes_to_expire": 10,
"sender_id": "BRIQ OTP",
"message_template": "Your verification code is {code}. It expires in {expiry} minutes."
}'
The platform calls the recipient and reads the code via TTS. There is no sender_id or message_template field — both are SMS-only and ignored on this channel.Channel-specific fields: none.Exact payload:{
"phone_number": "255712345678",
"app_key": "YOUR_APP_KEY",
"delivery_method": "call",
"otp_length": 6,
"minutes_to_expire": 10
}
curl -X POST https://karibu.briq.tz/v1/otp/request \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone_number": "255712345678",
"app_key": "YOUR_APP_KEY",
"delivery_method": "call",
"otp_length": 6,
"minutes_to_expire": 10
}'
Delivers the OTP through the platform-managed briq_otp WhatsApp Business template. There is no sender_id or message_template field — sender resolution is automatic and the wording is fixed by the approved Meta template.Sender resolution order (you don’t pass any sender field; the dispatcher picks one):
- A non-default WhatsApp sender owned by your developer user with an APPROVED
briq_otp template.
- The sender whose Infobip
phone_number_id matches the platform-wide WHATSAPP_PHONE_NUMBER_ID env value.
- The legacy
is_default = TRUE sender row.
Channel-specific fields: none.Exact payload:{
"phone_number": "255712345678",
"app_key": "YOUR_APP_KEY",
"delivery_method": "whatsapp",
"otp_length": 6,
"minutes_to_expire": 10
}
curl -X POST https://karibu.briq.tz/v1/otp/request \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone_number": "255712345678",
"app_key": "YOUR_APP_KEY",
"delivery_method": "whatsapp",
"otp_length": 6,
"minutes_to_expire": 10
}'
WhatsApp-specific 400s. If the dispatcher cannot resolve a sender + APPROVED briq_otp template (e.g. the platform default isn’t configured, or a custom one is in DRAFT/PAUSED), the response is success: false with a message describing which check failed (NO_DEFAULT_SENDER, TEMPLATE_NOT_APPROVED). Fall back to SMS in your client when this happens.
Error responses
| HTTP | Cause | Shape |
|---|
| 400 | invalid phone, send failure, unsupported delivery_method, WhatsApp dispatcher errors (NO_DEFAULT_SENDER, TEMPLATE_NOT_APPROVED) | envelope with success: false and a message |
| 401 | missing/expired X-API-Key | {"detail": "Invalid or expired API key"} |
| 403 | wrong host, app_key not yours, app_key mismatch with the bound app, workspace not dev-accessible | {"detail": "..."} |
| 422 | malformed body (e.g. non-digit phone, missing fields) | FastAPI validation error |
For the full cross-endpoint error reference, see Error scenarios.
Best practices
- Validate the phone client-side before calling the API: digits-only, country code present, plausible length.
- Store
data.expires_at so your UI can drive a countdown timer; consider polling /v1/otp/status instead of triggering re-sends speculatively.
- Cascade channels for reliability. A common pattern: try
"whatsapp" first; on a dispatcher 400, fall back to "sms"; offer "call" as a manual third option.
- Don’t promise custom WhatsApp wording. The
briq_otp template is fixed by Meta — sender_id and message_template are silently ignored on "whatsapp" and "call".
- Rate-limit your own users. The API does not enforce per-phone request limits — your application should.
- Never log the plaintext code. It only exists in transit to the recipient.
What’s next