Skip to main content
A complete reference for integrating Briq’s One-Time Password (OTP) API. The API lets you generate, deliver, verify, resend, and invalidate short numeric codes over SMS, voice call, or WhatsApp, scoped to a Developer App you own.
  • API surface: https://karibu.briq.tz/v1/otp/*
  • Audience: backend engineers integrating phone-based verification (signup, login, transaction confirmation).
  • Version: v1.1

1. Overview

The Karibu OTP API is a stateless HTTP service that:
  • Generates a numeric OTP (default 6 digits) and delivers it to a phone number via SMS (default), voice call, or WhatsApp.
  • Verifies a code submitted by the end user, with a hard cap of 3 attempts per active OTP.
  • Manages lifecycle: resend, invalidate, and inspect the currently active OTP for a (phone, app) pair.
Codes are stored hashed (bcrypt) — the server never returns the plaintext code in any response. Only the recipient sees it via the SMS, call, or WhatsApp message.

Channel comparison

Channeldelivery_methodBest forCustomisable per-call?
SMS"sms"Default; broadest reachsender_id, message_template
Voice call"call"SMS-restricted regions, accessibilitynone — TTS reads the code
WhatsApp"whatsapp"App-installed users, rich-format follow-upsnone — uses the platform-approved briq_otp template

2. Prerequisites

Before integrating, make sure you have:
  • A Briq account with a workspace.
  • An active developer API key (X-API-Key). Generate one in the Briq dashboard.
  • At least one Developer App registered against your workspace. Each app gives you:
    • app_id — UUID, used optionally in the X-App-ID header.
    • app_key — string token, sent in the body of every OTP request.
  • The workspace must allow developer access (toggle in workspace settings).
  • Recipient phone numbers in E.164 digits-only format (no +, no spaces, e.g. 255712345678).
If you don’t yet have a Developer App, see Developer Apps before continuing.

3. Authentication & host

Base URL

https://karibu.briq.tz
All OTP endpoints are versioned under /v1/otp/.

Required headers

HeaderRequiredNotes
X-API-KeyyesYour developer API key. Must be active and not expired.
X-App-IDnoA specific Developer App UUID. If your API key is bound to an app, this — when sent — must match that app, or the request fails with 403.
Content-Typeyes (POST)application/json
HostyesMust be karibu.briq.tz (or docs.briq.tz). Other hosts are rejected with 403 outside development/sandbox envs.

App-scoping rules

Every OTP endpoint requires an app_key field in the body (or query, for /status):
  • The app_key must resolve to a Developer App you own.
  • If your X-API-Key is bound to a specific Developer App, the body’s app_key must resolve to the same app — otherwise you get 403 (app_key does not match the API key's linked developer app).
  • The Developer App’s workspace must allow developer access for your user — otherwise 403 (Workspace does not allow developer access.).
These checks run on every endpoint described below.

4. Standard response envelope

Every endpoint returns the same JSON envelope:
{
  "success": true,
  "message": "Human-readable summary",
  "data": { /* endpoint-specific payload, or null */ },
  "status_code": 200
}
  • successtrue for normal operation, false for handled errors / “not found” cases embedded in the envelope.
  • message — short human-readable string suitable for logging.
  • data — endpoint-specific. May be null.
  • status_code — mirrors the HTTP status code returned.
Errors raised by FastAPI itself (401, 403 from auth/host gates, 422 from validation) follow the standard {"detail": "..."} shape, not the envelope above.

5. Behavioral rules (read this first)

These rules apply across all endpoints. Designing your client around them upfront prevents most integration bugs.
RuleDetail
Single active OTP per (phone, app)request and resend invalidate any prior unused, unexpired OTP for the same phone scoped to the same Developer App before issuing a new one.
Hashed at restOTP codes are bcrypt-hashed. The plaintext is delivered only via SMS, call, or WhatsApp to the recipient. The server will never return it in any response.
3-attempt cap on verifyAfter 3 wrong codes the OTP is auto-locked (marked used) and remaining_attempts becomes 0. The user must request a new OTP.
Default expiry10 minutes. Configure per call via minutes_to_expire.
Default OTP length6 digits. Configure per call via otp_length.
Delivery method"sms" (default), "call", or "whatsapp". Anything else returns 400.
Sender IDSMS only. Defaults to the platform-configured OTP_DEFAULT_SENDER_ID if omitted. Ignored on "call" and "whatsapp".
Message templateSMS only. Optional. Must include {code}. {expiry} is also substituted. If {code} is missing the server falls back to the default template. Ignored on "call" and "whatsapp".
WhatsApp sender resolutionWhatsApp only. The dispatcher resolves which sender number to dispatch from in this order: (1) a non-default WhatsApp sender owned by your developer user with an APPROVED briq_otp template; (2) the sender whose Infobip phone_number_id matches the platform-wide WHATSAPP_PHONE_NUMBER_ID env value; (3) the legacy is_default = TRUE sender row. You don’t pass any sender field — the resolution is automatic.
Phone formatE.164 digits only, no +. e.g. 255712345678. Invalid input → 400 "Invalid phone number".
Cross-app isolationStatus, verify, resend, and invalidate operate only on OTPs issued under the same Developer App. An OTP issued under app A cannot be verified, queried, or invalidated via app B.

6. Endpoint reference

Each endpoint section below documents purpose, contract, error semantics, and copy-pasteable client snippets in cURL, Python (requests), Node.js (fetch), and PHP (cURL extension).

6.1 POST /v1/otp/request

Generate a fresh OTP and deliver it to the phone. Method & path: POST https://karibu.briq.tz/v1/otp/request Headers: X-API-Key, Content-Type: application/json. Optional: X-App-ID. Common fields (every channel):
FieldTypeRequiredDefaultNotes
phone_numberstring (E.164 digits)yese.g. 255712345678
app_keystringyesfrom your Developer App
delivery_methodstringyes (recommended)"sms"one of "sms", "call", "whatsapp"
otp_lengthintno6code length
minutes_to_expireintno10TTL
The remaining fields are channel-specific. Pick the tab below that matches the channel you’re sending on — each one shows the exact payload, the success response shape, and copy-pasteable client snippets. Success response (same shape across channels):
{
  "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.
Error responses (same across channels):
HTTPCauseShape
400invalid phone, send failure, unsupported delivery_method, WhatsApp dispatcher returned an error (e.g. NO_DEFAULT_SENDER, TEMPLATE_NOT_APPROVED)envelope with success: false and a message
401missing/expired X-API-Key{"detail": "Invalid or expired API key"}
403wrong host, app_key not yours, app_key mismatch with the bound app, workspace not dev-accessible{"detail": "..."}
422malformed body (e.g. non-digit phone, missing fields)FastAPI validation error
Behavior: invalidates the prior active OTP for (phone, app) before issuing the new one. Returns 200 only after the dispatch is accepted by the upstream provider.

Channel-specific payload & code samples

Default channel. Customisable per call via sender_id and message_template.Channel-specific fields:
FieldTypeRequiredDefaultNotes
sender_idstringnoOTP_DEFAULT_SENDER_IDThe SMS sender ID shown to the recipient. Must be approved for your account.
message_templatestringnoserver defaultMust 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."
  }'

6.2 POST /v1/otp/verify

Check a code submitted by the end user. Method & path: POST https://karibu.briq.tz/v1/otp/verify Headers: X-API-Key, Content-Type: application/json. Request body:
FieldTypeRequiredNotes
phone_numberstring (E.164 digits)yes
app_keystringyesmust match the app the OTP was issued under
codestringyesplaintext code submitted by the user
Success 200:
{
  "success": true,
  "message": "OTP verified successfully.",
  "data": { "verified_at": "2026-05-08T12:35:21.000000" },
  "status_code": 200
}
The OTP is marked used; subsequent verifies for the same code return success: false. Handled errors (envelope, success: false):
messagedata.remaining_attemptsWhen
"No valid OTP found"0no active OTP, expired, or already used
"Invalid OTP code"2, 1, 0wrong code; counter decrements
"Max verification attempts reached"03rd wrong attempt — OTP is now locked
"Invalid phone number"malformed phone
Auth/host errors (401, 403) and validation (422) are the same as on /request.

Code samples

curl -X POST https://karibu.briq.tz/v1/otp/verify \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phone_number": "255712345678",
    "app_key": "YOUR_APP_KEY",
    "code": "123456"
  }'

6.3 POST /v1/otp/resend

Issue a new OTP after invalidating the previous one. Body shape and channel-specific fields are identical to §6.1 /request; only the path changes. Method & path: POST https://karibu.briq.tz/v1/otp/resend Success 200:
{
  "success": true,
  "message": "OTP resent successfully.",
  "data": { "expires_at": "2026-05-08T12:45:01.000000" },
  "status_code": 200
}
Error responses: identical to /request.
request vs resend — what’s the difference? Functionally both invalidate any prior active OTP and dispatch a new one. Use /resend when the end user explicitly clicks “Resend code”; this lets you treat it differently in your analytics, rate-limits, or UI without changing payloads.
Switching channels on resend is allowed — e.g. user clicks “I didn’t get the SMS, send via WhatsApp instead.” Just send the new delivery_method. The previous active OTP is invalidated regardless of which channel issued it.

Channel-specific payload & code samples

cURL
curl -X POST https://karibu.briq.tz/v1/otp/resend \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phone_number": "255712345678",
    "app_key": "YOUR_APP_KEY",
    "delivery_method": "sms",
    "sender_id": "BRIQ OTP",
    "message_template": "Your verification code is {code}. It expires in {expiry} minutes."
  }'
Python
resp = requests.post(
    f"{BASE_URL}/v1/otp/resend",
    headers={"X-API-Key": API_KEY, "Content-Type": "application/json"},
    json={
        "phone_number": "255712345678",
        "app_key": APP_KEY,
        "delivery_method": "sms",
        "sender_id": "BRIQ OTP",
        "message_template": "Your verification code is {code}. It expires in {expiry} minutes.",
    },
    timeout=15,
)
print(resp.json())
Node.js
const resp = await fetch(`${BASE_URL}/v1/otp/resend`, {
  method: "POST",
  headers: { "X-API-Key": API_KEY, "Content-Type": "application/json" },
  body: JSON.stringify({
    phone_number: "255712345678",
    app_key: APP_KEY,
    delivery_method: "sms",
    sender_id: "BRIQ OTP",
    message_template: "Your verification code is {code}. It expires in {expiry} minutes.",
  }),
});
console.log(await resp.json());
PHP
$ch = curl_init("$baseUrl/v1/otp/resend");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        "X-API-Key: $apiKey",
        "Content-Type: application/json",
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        "phone_number"     => "255712345678",
        "app_key"          => $appKey,
        "delivery_method"  => "sms",
        "sender_id"        => "BRIQ OTP",
        "message_template" => "Your verification code is {code}. It expires in {expiry} minutes.",
    ]),
]);
echo curl_exec($ch);
curl_close($ch);

6.4 POST /v1/otp/invalidate

Force-expire any active OTP for a phone scoped to your Developer App. Useful on logout, security events, or when the user changes their phone number. Method & path: POST https://karibu.briq.tz/v1/otp/invalidate Request body:
FieldTypeRequired
phone_numberstring (E.164 digits)yes
app_keystringyes
Success 200:
{
  "success": true,
  "message": "OTP invalidated successfully.",
  "data": null,
  "status_code": 200
}
Idempotent. Calling this when there is no active OTP also returns 200 — it simply has nothing to invalidate.

Code samples

curl -X POST https://karibu.briq.tz/v1/otp/invalidate \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phone_number": "255712345678",
    "app_key": "YOUR_APP_KEY"
  }'

6.5 GET /v1/otp/status

Inspect the currently active OTP for a phone, scoped to your Developer App, without triggering a send. Useful for UI countdowns, diagnostics, and idempotent UX (e.g. “an OTP was already sent — please check your phone”). Method & path: GET https://karibu.briq.tz/v1/otp/status Query parameters:
ParamRequired
phone_numberyes
app_keyyes
Success 200:
{
  "success": true,
  "message": "OTP status retrieved.",
  "data": {
    "is_valid": true,
    "expires_at": "2026-05-08T12:45:01.000000",
    "remaining_attempts": 3
  },
  "status_code": 200
}
404 (no active OTP):
{
  "success": false,
  "message": "No active OTP found.",
  "data": null,
  "status_code": 404
}
The HTTP response body wraps status_code: 404, but the underlying HTTP response itself is 200 OK (the envelope shape applies here too). Inspect body.success and body.status_code, not the raw HTTP status, when handling this endpoint.

Code samples

curl -G https://karibu.briq.tz/v1/otp/status \
  -H "X-API-Key: YOUR_API_KEY" \
  --data-urlencode "phone_number=255712345678" \
  --data-urlencode "app_key=YOUR_APP_KEY"

7. End-to-end flow

A typical phone-verification flow:
  1. User enters phone in your UI → your backend calls POST /v1/otp/request with the appropriate delivery_method.
  2. User receives SMS, voice call, or WhatsApp message with the plaintext code.
  3. User submits code in your UI → your backend calls POST /v1/otp/verify.
    • On success: true → mark phone verified, continue signup/login.
    • On success: false → show error and use data.remaining_attempts to drive UX.
  4. 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.
  5. User logs out / changes phone → your backend calls POST /v1/otp/invalidate.
  6. Optional UI affordances: poll GET /v1/otp/status to drive countdown timers or detect that an OTP is already in-flight before issuing a new one.
For narrative walkthroughs, see Requesting OTP codes, Validating OTP codes, and Managing OTP lifecycle.

8. Error scenarios — quick reference

ScenarioHTTPBody shapeRecommended client action
Missing/expired X-API-Key401{"detail": "Invalid or expired API key"}Surface “Authentication failed” — operator should rotate the key.
API key expired401{"detail": "API key expired"}Same — operator action.
Wrong host403{"detail": "...only available for the developer domain..."}Fix your client’s Host / base URL.
app_key not yours / invalid403{"detail": "Invalid app_key"} or "Invalid developer app or workspace."Verify app_key matches an app under your account.
app_key mismatch with bound API key403{"detail": "app_key does not match the API key's linked developer app"}Either send the matching app_key, or use an API key not bound to an app.
Workspace blocks dev access403{"detail": "Workspace does not allow developer access."}Toggle developer access for the workspace in the Briq UI.
Bad phone format400 / 422envelope success: false, "Invalid phone number" or FastAPI 422Re-validate digits-only E.164 client-side before sending.
Unsupported delivery_method400envelope with success: falseUse "sms", "call", or "whatsapp".
WhatsApp dispatcher cannot resolve sender/template400envelope success: false with NO_DEFAULT_SENDER / TEMPLATE_NOT_APPROVED in the messageFall back to SMS automatically; surface a “try another channel” UI.
Wrong code on /verify200 (envelope-400)envelope success: false, "Invalid OTP code", data.remaining_attempts=NDecrement UI counter; prompt resend at 0.
Max attempts exceeded200 (envelope-400)"Max verification attempts reached", remaining_attempts=0Force-resend; do not allow more verifies on this OTP.
OTP expired or already used200 (envelope-400)"No valid OTP found", remaining_attempts=0Prompt user to request a new OTP.
/status for unknown phone/app200 (envelope-404)envelope success: false, status_code: 404Treat as “no active OTP”.

9. Best practices

  • Never log the plaintext code. Treat it as a credential. Don’t echo it from your verify form, don’t store it in analytics, don’t include it in error reports.
  • Do not ship app_key to untrusted clients. Hold it server-side; have your frontend hit your backend, which then calls Karibu.
  • Respect the 3-attempt cap in your UI. Disable the verify button after 3 failures and surface “Resend code” prominently.
  • Handle remaining_attempts: 0 distinctly from remaining_attempts > 0. They demand different UX (lockout vs retry).
  • Invalidate on logout. It’s safe to call even if you’re unsure an OTP is active.
  • Default to delivery_method: "sms". Use "call" for SMS-restricted regions or accessibility; use "whatsapp" for app-installed users with chat-first habits.
  • Cascade channels for reliability. A common pattern: try "whatsapp" first; if the dispatcher returns a 400 (template not approved, dispatcher misconfigured) fall back to "sms", and offer "call" as a manual third option.
  • Sender ID is SMS-only. A misconfigured sender_id may silently route through the platform default; verify deliverability with a test number before launch. The field is ignored on "call" and "whatsapp".
  • WhatsApp template wording is fixed. Don’t ship a UI that promises a custom message body when sending over WhatsApp — only briq_otp is dispatched, so the recipient sees the platform-approved Meta template.
  • Rate-limit your own users. The API does not enforce per-phone request rate limits — your application should.
See also Best practices and Security notes for cross-product guidance.

10. Changelog

  • v1.1 — added WhatsApp delivery channel (delivery_method: "whatsapp"). Routes through the platform-managed briq_otp template via the WhatsApp dispatcher; sender resolution is automatic (developer-owned sender → env-pinned platform default → is_default = TRUE fallback). sender_id and message_template are ignored on this channel. /request and /resend payload tables split into channel-specific tabs.
  • v1 — initial public surface: request, verify, resend, invalidate, status. SMS + voice call delivery. 3-attempt cap. Single-active-OTP per (phone, app).