A complete reference for integrating Briq’s WhatsApp Business API. Send text, template, media, and interactive messages, read conversation history, list senders and templates, and receive real-time delivery events (sent -> delivered -> read / failed) on your own webhook - all scoped to a Developer App you own.
- API surface:
https://karibu.briq.tz/v1/whatsapp/* and https://karibu.briq.tz/v1/webhooks/*
- Audience: backend engineers integrating WhatsApp messaging (notifications, OTPs, customer care, interactive flows).
- Version:
v1
If you only read one thing, read Section 5 Behavioral rules - the 24-hour window and the template requirement are the two rules that trip up most integrations.
1. Overview
The Briq WhatsApp API is a stateless HTTP service that lets your backend:
- Send four kinds of outbound messages: freeform text, pre-approved templates, media (image/document/video), and interactive messages (buttons/lists).
- Read your WhatsApp data: senders (your business numbers), templates (approved blueprints), and conversations (threads with their messages and 24-hour window state).
- Receive delivery events -
whatsapp.sent, whatsapp.delivered, whatsapp.read, whatsapp.failed - pushed to a webhook URL you register.
Briq routes your messages through an underlying provider (Meta Cloud API or Infobip) and normalizes everything behind one consistent API. You never talk to the provider directly, and the webhook event names are identical regardless of which provider delivered the message.
Available today vs. upcoming
| Capability | Status |
|---|
| Send text / template / media / interactive | Available |
| Mark inbound messages as read | Available |
| List senders, templates, conversations, and conversation messages | Available |
Register webhooks and receive sent / delivered / read / failed events | Available |
| Inspect & retry webhook deliveries | Available |
Receive inbound customer messages on your webhook (whatsapp.received) | Upcoming - poll the Conversations API meanwhile |
Native WhatsApp test webhook event (the test endpoint currently emits sms.sent) | Upcoming |
| Bulk / campaign sends via API | Upcoming |
2. Prerequisites
- A Briq account with a workspace, and WhatsApp enabled for that workspace (at least one approved sender number / WABA configured). New senders are onboarded via the Briq dashboard.
- An active developer API key (
X-API-Key). Generate one in the Briq dashboard.
- A Developer App linked to a workspace. Every WhatsApp endpoint is workspace-scoped; a key with no workspace is rejected with 403.
- At least one approved template if you intend to start conversations or message users outside the 24-hour window.
- Recipient phone numbers in E.164 digits-only format - no
+, no spaces (e.g. 255712345678).
3. Authentication & host
Base URL
All endpoints are versioned under /v1/.
| Header | Required | Notes |
|---|
X-API-Key | yes | Your developer API key. Must be active and not expired. Missing/invalid -> 401. |
X-App-ID | no | A specific Developer App UUID. If your key is bound to an app, then - when you send X-App-ID - it must match that app, or the request is rejected with 403. |
Content-Type | yes (POST/PATCH) | application/json |
Host | yes | Must be karibu.briq.tz (or docs.briq.tz). Other hosts -> 403 outside development/sandbox. |
Workspace requirement
Every WhatsApp endpoint resolves your workspace from the app linked to your API key. If the key is not linked to a workspace you receive 403 (WORKSPACE_REQUIRED on message endpoints; a raw {"detail": "API key is not linked to a workspace."} on read endpoints). All senders, templates, conversations, and messages you can see are scoped to that one workspace.
4. Response conventions
Briq’s WhatsApp surface uses two response shapes. Know which one each endpoint uses so you parse correctly.
Enveloped responses - the Messages endpoints
POST /v1/whatsapp/messages/* (send-text, send-template, send-media, send-interactive, and mark-read) return a standard envelope:
{
"success": true,
"data": { "...": "endpoint-specific payload" },
"errors": null,
"request_id": "f1e2d3c4-b5a6-7890-1234-567890abcdef"
}
On error:
{
"success": false,
"data": null,
"errors": [
{ "code": "WINDOW_CLOSED", "message": "The 24-hour conversation window has closed. Use a template message instead.", "field": null }
],
"request_id": "f1e2d3c4-b5a6-7890-1234-567890abcdef"
}
success - true for 2xx, false otherwise.
data - payload on success, null on error.
errors - array of {code, message, field} on error, null on success.
request_id - unique per response. Quote it when contacting support.
Body validation failures (422) for any /v1/whatsapp/* path are returned in this envelope with errors[].code = "VALIDATION_FAILED" and field set to the offending field.
Bare responses - the read endpoints
GET /v1/whatsapp/senders/*, GET /v1/whatsapp/conversations/*, and GET /v1/whatsapp/templates/* return the resource directly - a JSON array or object - with no envelope. Their errors use the raw framework shape:
{ "detail": "Sender not found in this workspace." }
The Webhook Management endpoints also use bare objects and {"detail": ...} errors.
Treat any non-2xx status as a failure regardless of body shape. On enveloped endpoints read errors[].code; on bare endpoints read detail.
5. Behavioral rules (read this first)
-
The 24-hour customer-care window. WhatsApp only lets a business send freeform messages (text, media, interactive) within 24 hours of the customer’s last inbound message. Outside that window you must send an approved template. Briq enforces this:
send-text / send-media / send-interactive return WINDOW_CLOSED (HTTP 422) when the window is closed (unless you pass check_window: false). send-template always works and opens/refreshes the window.
-
Templates must be APPROVED. You can only send a template whose admin status is
APPROVED. Sending a non-approved or unknown template returns TEMPLATE_NOT_APPROVED / TEMPLATE_NOT_FOUND_OR_UNAPPROVED.
-
Sends are accepted, then confirmed asynchronously. A successful send returns
200 (status: "sent") when the provider acknowledged immediately, or 202 (status: "pending") when queued to Briq’s async engine. Treat both as success and rely on webhook events for the final outcome. The data.message_id returned by the send is the same id that appears in every subsequent webhook - use it to correlate.
-
Webhook events only fire for messages sent via this API.
delivered / read events are emitted only for messages your API key sent. Messages sent from the dashboard do not notify your webhook.
-
Rate limits. Send endpoints are limited per API key (default 60 requests/minute). Exceeding returns 429 with a
Retry-After header. Back off and retry. The limiter is fail-open: if Redis is unavailable, requests are allowed through.
-
Recipients are digits-only E.164. e.g.
255712345678. A leading + is stripped automatically.
6. Quick start (5 minutes)
# 1. Find a sender (your WhatsApp business number) in your workspace.
curl -s https://karibu.briq.tz/v1/whatsapp/senders \
-H "X-API-Key: YOUR_API_KEY"
# 2. Register a webhook so you receive delivery events.
curl -s -X POST https://karibu.briq.tz/v1/webhooks/ \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "app_id": "YOUR_APP_ID", "service_type": "whatsapp", "url": "https://api.yourdomain.com/briq/webhooks/whatsapp" }'
# 3. Fetch the signing secret (used to verify X-Briq-Signature on incoming events).
curl -s https://karibu.briq.tz/v1/webhooks/WEBHOOK_ID/secret \
-H "X-API-Key: YOUR_API_KEY"
# 4a. Inside the 24h window: send a freeform text.
curl -s -X POST https://karibu.briq.tz/v1/whatsapp/messages/send-text \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "sender_id": "SENDER_UUID", "recipient": "255712345678", "body": "Hello from Briq!" }'
# 4b. Outside the window (or first contact): send an approved template.
curl -s -X POST https://karibu.briq.tz/v1/whatsapp/messages/send-template \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "sender_id": "SENDER_UUID", "template_name": "briq_otp", "recipient": "255712345678", "variables": { "otp": "654321" } }'
You will receive whatsapp.sent, then whatsapp.delivered, then whatsapp.read (or whatsapp.failed) on your webhook.
7. Messages API
All endpoints are mounted under /v1/whatsapp/messages and return the standard envelope. A successful send returns HTTP 200 (data.status = "sent") when the provider accepted immediately, or HTTP 202 (data.status = "pending") when queued; treat both as success.
Behaviors shared by all send endpoints
- 200 vs 202. If a
provider_message_id is present the message went out synchronously (200/sent). Otherwise it was queued (202/pending, provider_message_id: null), and the provider id is filled in later via webhook.
- 24-hour window.
send-text, send-media, send-interactive require an OPEN window unless check_window: false. send-template is not subject to the window.
- Recipient normalization.
recipient is digits-only E.164; a leading + is stripped.
- Sender resolution. When
sender_id is omitted (where allowed), the workspace’s first active sender is used.
- Rate limiting. 60 req/min per key by default;
429 + Retry-After when exceeded.
- Usage metering. Every successful send (and read receipt) is metered against your API key.
Common success payload (data)
| Field | Type | Description |
|---|
message_id | UUID | null | Briq’s internal conversation message id. Matches data.message_id in webhooks. |
conversation_id | UUID | null | The conversation this message belongs to. |
sender_id | UUID | null | The sender the message was dispatched from. |
provider_message_id | string | null | Provider message id (wamid). Present on 200/sent; null on 202/pending. |
status | string | "sent" (HTTP 200) or "pending" (HTTP 202). |
7.1 POST /v1/whatsapp/messages/send-text - Send a freeform text
Freeform text can only be delivered inside an open 24-hour window, so this endpoint enforces the window by default.
| Field | Type | Required | Constraints | Description |
|---|
sender_id | UUID | No | Valid sender in your workspace | Falls back to the first active sender when omitted. |
recipient | string | Yes | 4-20 chars, digits-only E.164 | Destination number. |
body | string | Yes | 1-4096 chars | The text content. |
check_window | boolean | No | Default true | Set false to skip the 24h window check. |
Success (200):
{
"success": true,
"data": {
"message_id": "f0e9d8c7-b6a5-4321-9876-543210fedcba",
"conversation_id": "11112222-3333-4444-5555-666677778888",
"sender_id": "8b1d6c2e-7c3a-4e2b-9f1a-2d4c6e8a0b12",
"provider_message_id": "wamid.HBgLMjU1NzEyMzQ1Njc4FQIAERgS...",
"status": "sent"
},
"errors": null,
"request_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
A 202 response is identical except provider_message_id: null and status: "pending".
curl -X POST "https://karibu.briq.tz/v1/whatsapp/messages/send-text" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sender_id": "8b1d6c2e-7c3a-4e2b-9f1a-2d4c6e8a0b12",
"recipient": "255712345678",
"body": "Hello from Briq! Your order has shipped.",
"check_window": true
}'
7.2 POST /v1/whatsapp/messages/send-template - Send a pre-approved template
Templates are the only way to message a recipient outside the 24-hour window, so this endpoint is not gated by the window check. The named sender must own a template with the given name, and that template must be APPROVED by Meta.
| Field | Type | Required | Constraints | Description |
|---|
sender_id | UUID | Yes | Must own the template | The sender that owns the template. |
template_name | string | Yes | 1-512 chars | Name of the approved template. |
recipient | string | Yes | 4-20 chars, digits-only E.164 | Destination number. |
variables | object (map<string,string>) | No | Default {} | Template variable values keyed by placeholder name. |
callback_data | string | No | Max 512 chars | Opaque data you can correlate with webhooks. |
curl -X POST "https://karibu.briq.tz/v1/whatsapp/messages/send-template" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sender_id": "8b1d6c2e-7c3a-4e2b-9f1a-2d4c6e8a0b12",
"template_name": "order_update",
"recipient": "255712345678",
"variables": { "1": "Asha", "2": "TZ-90211" },
"callback_data": "order-90211"
}'
Use a template to (re)open a conversation, after which freeform messages become deliverable. Provide every required variable; missing keys cause TEMPLATE_PARAM_COUNT_MISMATCH. See Section 9 Templates API for how to map a template’s params onto variables.
7.3 POST /v1/whatsapp/messages/send-media - Send image, document, or video
Media is referenced by URL; the provider fetches it, so media_url must be publicly reachable. Requires an open 24h window by default.
| Field | Type | Required | Constraints | Description |
|---|
sender_id | UUID | No | Valid sender | Falls back to the first active sender when omitted. |
recipient | string | Yes | 4-20 chars, digits-only E.164 | Destination number. |
media_kind | string (enum) | Yes | IMAGE, DOCUMENT, VIDEO | Case-sensitive. |
media_url | string | Yes | Min length 1 | Publicly reachable URL of the asset. |
caption | string | No | Max 1024 chars | Optional caption. |
filename | string | No | Max 255 chars | Optional filename (useful for documents). |
check_window | boolean | No | Default true | Set false to skip the window check. |
curl -X POST "https://karibu.briq.tz/v1/whatsapp/messages/send-media" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sender_id": "8b1d6c2e-7c3a-4e2b-9f1a-2d4c6e8a0b12",
"recipient": "255712345678",
"media_kind": "DOCUMENT",
"media_url": "https://files.example.com/invoices/INV-90211.pdf",
"caption": "Your invoice",
"filename": "INV-90211.pdf"
}'
7.4 POST /v1/whatsapp/messages/send-interactive - Send an interactive message
Sends an interactive WhatsApp message (button/list) using a raw Meta interactive payload you supply. Requires an open 24h window by default.
| Field | Type | Required | Constraints | Description |
|---|
sender_id | UUID | No | Valid sender | Falls back to the first active sender when omitted. |
recipient | string | Yes | 4-20 chars, digits-only E.164 | Destination number. |
interactive | object | Yes | Raw Meta interactive payload | Passed through to the provider as-is. |
check_window | boolean | No | Default true | Set false to skip the window check. |
curl -X POST "https://karibu.briq.tz/v1/whatsapp/messages/send-interactive" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sender_id": "8b1d6c2e-7c3a-4e2b-9f1a-2d4c6e8a0b12",
"recipient": "255712345678",
"interactive": {
"type": "button",
"body": { "text": "Confirm your appointment?" },
"action": {
"buttons": [
{ "type": "reply", "reply": { "id": "yes", "title": "Yes" } },
{ "type": "reply", "reply": { "id": "no", "title": "No" } }
]
}
}
}'
Build the interactive object to Meta’s WhatsApp interactive message spec - it is passed through unchanged. A malformed payload is rejected by the provider as INFOBIP_FAILURE (503).
7.5 POST /v1/whatsapp/messages/{message_id}/read - Mark an inbound message as read
Sends a read receipt for an inbound message you received. Only inbound messages belonging to a sender in your workspace can be marked read. Always responds synchronously with HTTP 200 (no pending variant) and is not rate-limited.
Path parameter: message_id (string, 1-355 chars) - the inbound wamid.
Success (200):
{
"success": true,
"data": { "message_id": "wamid.HBgLMjU1NzEyMzQ1Njc4FQIAEhgU", "status": "read_receipt_sent" },
"errors": null,
"request_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
curl -X POST "https://karibu.briq.tz/v1/whatsapp/messages/wamid.HBgLMjU1NzEyMzQ1Njc4FQIAEhgU/read" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json"
| Code | HTTP | When |
|---|
INVALID_MESSAGE | 422 | The message is outbound - read receipts are only valid for inbound messages. |
MESSAGE_NOT_FOUND | 404 | No message with that id exists. |
FORBIDDEN | 403 | The message does not belong to your sender/workspace. |
8. Senders API
Senders are the WhatsApp business phone numbers configured for your workspace. Pick which sender to use by passing its sender_id; if omitted on a send, the workspace’s first active sender is used.
These read endpoints return bare models - no envelope. The list returns a JSON array; get-one returns a single object. Errors use {"detail": "..."}.
8.1 GET /v1/whatsapp/senders - List senders
Query parameters: is_active (bool, optional), is_default (bool, optional).
curl "https://karibu.briq.tz/v1/whatsapp/senders?is_active=true" \
-H "X-API-Key: YOUR_API_KEY"
Success (200):
[
{
"id": "3f1c2b8a-9d4e-4a7b-bf2c-1e6a0d5c4b21",
"user_id": "usr_8K2p9Qd",
"whatsapp_account_id": "a02b7c19-4e8d-4c0a-9f31-2b6d5e8a1c44",
"phone_number_id": "109876543210987",
"display_number": "255700000001",
"label": "Support line",
"is_default": true,
"is_active": true,
"is_deleted": false,
"created_at": "2026-01-15T09:32:10.123456Z",
"updated_at": "2026-03-02T14:05:48.987654Z"
}
]
Sender object fields
| Field | Type | Description |
|---|
id | UUID | Unique identifier. Use as sender_id when sending. |
user_id | string | Owner user_id. |
whatsapp_account_id | UUID | null | Owning WABA account ID. |
phone_number_id | string | Provider-side phone-number ID. |
display_number | string | Display number (E.164 without +). |
label | string | null | Optional label. |
is_default | boolean | Whether this is the default sender. |
is_active | boolean | Whether eligible for sending. |
is_deleted | boolean | Whether soft-deleted. |
created_at / updated_at | datetime | Timestamps. |
8.2 GET /v1/whatsapp/senders/{sender_id} - Get one sender
Returns a single sender object (same shape). A sender_id belonging to another workspace returns 404 ({"detail": "Sender not found in this workspace."}).
curl "https://karibu.briq.tz/v1/whatsapp/senders/3f1c2b8a-9d4e-4a7b-bf2c-1e6a0d5c4b21" \
-H "X-API-Key: YOUR_API_KEY"
9. Conversations API
A conversation is the message thread between one of your senders and a single recipient. The 24-hour window state (is_active) is tracked on the conversation. Since inbound webhook events are upcoming, polling these endpoints is the current way to read inbound messages and observe window state.
These endpoints are read-only and return bare models; errors are raw {"detail": "..."}.
9.1 GET /v1/whatsapp/conversations - List conversations
Query parameters: sender_id (UUID), is_active (bool), limit (1-100, default 50), offset (>=0, default 0).
Success (200):
{
"items": [
{
"id": "b1d8f2a0-3c4e-4a1b-9f2c-7d6e5a4b3c2d",
"whatsapp_account_id": "a0c1e2f3-4567-8901-abcd-ef0123456789",
"whatsapp_sender_number_id": "c2d3e4f5-6789-0123-abcd-ef0123456789",
"recipient_phone": "+255712345678",
"is_active": true,
"last_message_at": "2026-05-30T10:15:30.000000Z",
"created_at": "2026-05-28T08:00:00.000000Z",
"updated_at": "2026-05-30T10:15:30.000000Z"
}
],
"total": 1,
"limit": 50,
"offset": 0
}
The wrapper is {items, total, limit, offset}. is_active indicates whether the 24h window is currently open (freeform sends allowed).
curl "https://karibu.briq.tz/v1/whatsapp/conversations?is_active=true&limit=50&offset=0" \
-H "X-API-Key: YOUR_API_KEY"
9.2 GET /v1/whatsapp/conversations/{conversation_id} - Get one conversation
Returns a single bare conversation object (same shape as a list item). Unknown IDs / other workspaces return 404.
9.3 GET /v1/whatsapp/conversations/{conversation_id}/messages - List messages
Returns messages (inbound and outbound) ordered by created_at ascending. Query parameters: limit (1-200, default 100), offset (>=0, default 0).
Success (200):
{
"items": [
{
"id": "f7e6d5c4-b3a2-4190-8f7e-6d5c4b3a2190",
"message_id": "wamid.HBgMMjU1NzEyMzQ1Njc4FQIAERgSN0E...",
"is_outbound": true,
"message_type": "text",
"content": { "body": "Hello! Your order has shipped." },
"status": "delivered",
"created_at": "2026-05-30T10:10:00.000000Z",
"paired_message_id": null
}
],
"total": 2,
"limit": 100,
"offset": 0
}
| Field | Type | Description |
|---|
id | UUID | Internal message identifier. |
message_id | string | Provider id (wamid.*) or a local- prefixed id if not yet assigned. |
is_outbound | bool | true if sent by your sender; false for inbound. |
message_type | string | e.g. text, template, image. |
content | object | Payload; shape depends on message_type. |
status | string | pending, queued, sent, delivered, read, failed. |
created_at | datetime | Creation time. |
paired_message_id | UUID | null | Linked message’s id, if any. |
Since inbound webhook delivery is upcoming, poll this endpoint to read inbound (is_outbound: false) messages.
10. Templates API
Message templates are pre-registered, Meta-approved structures you must use to start a conversation or message a user outside the 24-hour window. Only templates whose admin status is APPROVED can be sent. These read endpoints return bare models.
10.1 GET /v1/whatsapp/templates - List templates
Cursor-paginated. Query parameters: sender_id (UUID), status (repeatable), category (repeatable: AUTH/AUTHENTICATION, UTILITY, MARKETING), language (repeatable, e.g. en_US), name_or_content (max 200 chars), cursor (opaque), limit (1-200, default 50), sort (updated_desc default, updated_asc, created_desc, name_asc, name_desc).
Success (200):
{
"items": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "order_confirmation",
"language": "en_US",
"category": "UTILITY",
"params": ["customer_name", "order_id"],
"details": { "components": [ { "type": "BODY", "text": "Hi {{1}}, your order {{2}} is confirmed." } ] },
"status": "APPROVED",
"is_deleted": false,
"template_type": "standard",
"parameter_format": "POSITIONAL",
"quality_score": "GREEN",
"whatsapp_sender_number_id": "9c1f0b2e-7d44-4a91-8f0a-1b2c3d4e5f60",
"created_at": "2026-05-01T09:12:00Z",
"updated_at": "2026-05-28T10:15:00Z",
"approved_at": "2026-05-02T14:00:00Z",
"last_sent_at": "2026-05-29T08:30:00Z",
"rejection_reason_code": null,
"rejection_reason_text": null
}
],
"cursor_next": "eyJ1cGRhdGVkX2F0IjoiMjAyNi0wNS0yOFQxMDoxNTowMCswMDowMCIsImlkIjoiM2ZhODVmNjQ...",
"total_count": 137
}
Key fields
| Field | Type | Description |
|---|
id | UUID | Template identifier. |
name | string | The template_name you pass to send-template. |
language | string | null | e.g. en_US. |
category | enum | null | AUTH, UTILITY, MARKETING. |
params | array | null | Ordered placeholder names -> keys of the variables object. |
details | object | null | Component/placeholder layout. |
status | enum | DRAFT, IN_REVIEW, APPROVED, PAUSED, REJECTED, DISABLED, IN_APPEAL, PENDING_DELETION, DELETED, LIMIT_EXCEEDED, ARCHIVED. Only APPROVED is sendable. |
whatsapp_sender_number_id | UUID | null | The sender that owns the template. |
quality_score | string | null | Meta quality rating (GREEN/YELLOW/RED). |
approved_at / last_sent_at | datetime | null | Lifecycle timestamps. |
Cursor pagination: request a page, then if cursor_next is non-null pass it back as the cursor query param. Stop when cursor_next is null. Keep sort and filters identical across pages. Many additional lifecycle/meta fields are present on each item but are not needed for sending.
curl "https://karibu.briq.tz/v1/whatsapp/templates?status=APPROVED&limit=50" \
-H "X-API-Key: YOUR_API_KEY"
10.2 GET /v1/whatsapp/templates/{template_id} - Get one template
Returns a single bare template (same shape). A 404 is returned both for unknown IDs and templates owned by another workspace.
10.3 Using a template to send
Map a listed APPROVED template’s fields onto the send-template body:
template_name: the template’s name
variables: an object keyed by the template’s ordered params
sender_id: the template’s whatsapp_sender_number_id
language: implied by the template you reference (not a send-body field)
{
"sender_id": "9c1f0b2e-7d44-4a91-8f0a-1b2c3d4e5f60",
"template_name": "order_confirmation",
"recipient": "255700000000",
"variables": { "customer_name": "Asha", "order_id": "A-10293" }
}
11. Webhook events (delivery lifecycle)
Briq pushes status events to your registered whatsapp webhook as a message you sent moves through its delivery lifecycle. To manage webhooks (register, secret, test, deliveries, retries) see the Karibu Webhooks API.
Prerequisites
- You have a registered
whatsapp webhook for the app (one per app per service_type).
- The message was originally sent via the Developer API (only API-originated messages are tagged and correlated back to your app - dashboard sends do not notify your webhook).
The four events
| Event | Meaning |
|---|
whatsapp.sent | The message left Briq and was accepted by the provider. |
whatsapp.delivered | The message reached the recipient’s device. |
whatsapp.read | The recipient opened/read the message. |
whatsapp.failed | The message could not be delivered (terminal). |
Lifecycle
POST /v1/whatsapp/... (send)
|
v
200 sent / 202 pending
|
v
whatsapp.sent -> whatsapp.delivered -> whatsapp.read
|
+--> whatsapp.failed (terminal)
Briq enforces a status-rank guard so a message never regresses (a late sent cannot overwrite delivered; failed is terminal). Not every message produces all four events.
Common envelope
{
"event": "whatsapp.sent",
"event_id": "wa_3f1c9a2b7e8d4f6a9c0b1d2e3f4a5b6c",
"app_id": "8a1f0c4e-2b3d-4e5f-9a8b-7c6d5e4f3a2b",
"service_type": "whatsapp",
"timestamp": "2026-05-30T11:02:14.512389+00:00",
"data": {
"message_id": "d6b1f0a2-7c3e-4d5a-9b8c-1e2f3a4b5c6d",
"conversation_id": "b2c3d4e5-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
"provider_message_id": "wamid.HBgLMjU1...",
"recipient": "+255700000000"
}
}
| Field | Type | Description |
|---|
event | string | One of the four event names. |
event_id | string | Unique id, prefixed wa_ + 32-char hex. Use for idempotency. |
app_id | UUID | The developer app that owns the message and webhook. |
service_type | string | Always "whatsapp". |
timestamp | string (ISO 8601) | UTC time with +00:00 offset. |
data.message_id | UUID | The Briq conversation-message UUID - identical to the send response’s data.message_id. |
data.conversation_id | UUID | The conversation the message belongs to. |
data.provider_message_id | string | null | Provider wamid; may be null. |
data.recipient | string | null | Recipient phone number; may be null. |
data.error_code | string | Only on whatsapp.failed. |
data.error_message | string | Only on whatsapp.failed. |
A whatsapp.failed event adds error_code and error_message inside data:
{
"event": "whatsapp.failed",
"event_id": "wa_7d6c5b4a39281706f5e4d3c2b1a09f8e",
"app_id": "8a1f0c4e-2b3d-4e5f-9a8b-7c6d5e4f3a2b",
"service_type": "whatsapp",
"timestamp": "2026-05-30T11:02:31.665018+00:00",
"data": {
"message_id": "d6b1f0a2-7c3e-4d5a-9b8c-1e2f3a4b5c6d",
"conversation_id": "b2c3d4e5-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
"provider_message_id": "wamid.HBgLMjU1...",
"recipient": "+255700000000",
"error_code": "131026",
"error_message": "Message undeliverable"
}
}
Verifying the signature
Every delivery carries an X-Briq-Signature header containing the HMAC-SHA256 of the raw request body, keyed by your webhook’s signing secret. Fetch the secret once via GET /v1/webhooks/{id}/secret and store it securely.
- Compute the HMAC over the raw request bytes - do not re-serialize parsed JSON.
- Use a constant-time comparison.
- Reject the request if the signature is missing or does not match.
import hashlib
import hmac
def verify_briq_signature(raw_body: bytes, signature_header: str, secret: str) -> bool:
expected = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature_header or "")
# In your handler, read the RAW body (not request.json):
# raw = request.get_data()
# sig = request.headers.get("X-Briq-Signature", "")
# if not verify_briq_signature(raw, sig, WEBHOOK_SECRET):
# abort(401)
Idempotency & ordering
Webhook delivery is at-least-once and not strictly ordered.
- Idempotency: dedupe on
event_id (retried deliveries reuse it).
- Ordering: do not assume
sent arrives before delivered; events may arrive out of order.
- Never regress status on your side: once
read, don’t downgrade to delivered/sent; once failed, treat as terminal.
- Key off
data.message_id to attach each event to the correct message (matches your send response).
Each emitted event is persisted as a delivery with max_attempts = 3 and retried on non-2xx. Respond quickly with a 2xx, then process asynchronously. Inspect and replay via the Webhook Management endpoints.
Upcoming
- Inbound customer messages (
whatsapp.received) - not yet delivered; poll the Conversations API meanwhile.
- Native WhatsApp test event -
POST /v1/webhooks/{id}/test currently emits a synthetic sms.sent, not a whatsapp.* event.
12. Error reference
HTTP status codes
| Status | Meaning |
|---|
200 | Success. For sends: provider acknowledged immediately (status: "sent"). |
202 | Accepted. Send queued to the async engine (status: "pending"); await webhook. |
204 | Success, no content (webhook delete). |
400 | Bad request - unusable input (no sender, template params mismatch, duplicate webhook). |
401 | Missing or invalid X-API-Key. |
403 | Workspace not linked, X-App-ID mismatch, or wrong Host. |
404 | Resource not found in your workspace / not owned by you. |
422 | Validation failed, window closed, template not approved, or invalid state transition. |
429 | Rate limit exceeded. Honor Retry-After. |
503 | Provider/temporarily unavailable or unexpected server error - safe to retry with backoff. |
Send error codes (enveloped errors[].code)
| Code | HTTP | When | What to do |
|---|
VALIDATION_FAILED | 422 | Body failed schema validation | Fix the indicated field. |
WORKSPACE_REQUIRED | 403 | API key not linked to a workspace | Link the app to a workspace. |
NO_ACTIVE_SENDER | 400 | No active sender (when sender_id omitted) | Configure/activate a sender, or pass sender_id. |
SENDER_NOT_FOUND | 404 | sender_id not in your workspace | Use a valid sender from the Senders API. |
SENDER_INACTIVE | 400 | Sender exists but is disabled | Re-enable it or choose another. |
NO_SENDER / NO_USER_SENDER | 400 / 404 | Resolved sender missing or inactive | Verify the sender. |
NO_TEMPLATE_LINK | 404 | Sender does not own a template with that name | Use a template the sender owns. |
NO_DEFAULT_SENDER | 400 | No active sender with that approved template | Configure an approved template on a sender. |
TEMPLATE_NOT_APPROVED | 422 | Template not yet approved by Meta | Wait for approval / pick an approved template. |
TEMPLATE_NOT_FOUND_OR_UNAPPROVED | 404 | Template unknown or not approved | Check the name and approval status. |
TEMPLATE_PARAM_COUNT_MISMATCH | 400 | variables don’t match the template’s params | Send exactly the template’s params. |
TEMPLATE_PAUSED_OR_DISABLED | 422 | Template paused/disabled (quality) | Use another template; fix quality. |
WINDOW_CLOSED | 422 | 24-hour window closed for a freeform send | Send an approved template to reopen. |
RECIPIENT_UNDELIVERABLE | 422 | Number unreachable | Verify the recipient. |
PHONE_NOT_REGISTERED | 422 | Number not on WhatsApp | Confirm the user has WhatsApp. |
AUTH_FAILURE | 503 | Provider auth problem | Retry; contact support if persistent. |
RETRYABLE_PROVIDER | 503 | Transient provider error | Retry with backoff. |
INFOBIP_FAILURE | 503 | Provider could not process | Retry with backoff. |
RATE_LIMIT_EXCEEDED | 429 | Too many requests for this key | Honor Retry-After. |
SERVICE_UNAVAILABLE | 503 | Unexpected server error | Retry with backoff. |
mark-read error codes
| Code | HTTP | When |
|---|
INVALID_MESSAGE | 422 | The message is outbound (you can only mark inbound messages read). |
MESSAGE_NOT_FOUND | 404 | Unknown message_id. |
FORBIDDEN | 403 | The message isn’t in your workspace. |
13. Changelog
| Version | Notes |
|---|
| v1 | Initial WhatsApp Developer API: send (text/template/media/interactive), mark-read, senders, templates, conversations, webhook management, and whatsapp.sent/delivered/read/failed events. |
Upcoming: inbound message webhooks (whatsapp.received), a native WhatsApp test webhook event, and bulk/campaign sends.