> ## Documentation Index
> Fetch the complete documentation index at: https://docs.briq.tz/llms.txt
> Use this file to discover all available pages before exploring further.

# Karibu Webhooks API

> Complete reference for managing Briq webhooks - register callback URLs, reveal signing secrets, send test events, and inspect, audit, and retry every delivery attempt. Shared across all channels (sms, voice, otp, whatsapp, email).

A complete reference for the **Webhook Management API**. These endpoints let you register a callback URL for a developer app, reveal its signing secret, send test events, and inspect, audit, and retry every delivery attempt.

* **API surface:** `https://karibu.briq.tz/v1/webhooks/*`
* **Audience:** backend engineers wiring Briq delivery events into their product.
* **Version:** `v1`

<Note>
  The `/v1/webhooks/*` routes are **shared across all channels**. The same routes serve `sms`, `voice`, `otp`, `whatsapp`, and `email` - you pick the channel with the `service_type` field when you create a webhook. For the WhatsApp event payloads delivered to these webhooks, see the [Karibu WhatsApp API reference](/Karibu-WhatsApp/index#webhook-events).
</Note>

***

## 1. Overview

A **webhook** is a callback URL Briq POSTs event payloads to when something happens to a message you sent. To receive events you:

1. **Create** a webhook for one of your developer apps, choosing a `service_type` (e.g. `whatsapp`).
2. **Fetch the signing secret** and store it, so you can verify the `X-Briq-Signature` header on incoming deliveries.
3. **Return `2xx`** quickly from your endpoint. Non-`2xx` responses, timeouts, and connection errors are treated as failures and retried.

Each emitted event is persisted as a **delivery** row and attempted by Briq's delivery engine. You can list deliveries, view a single delivery's full payload, see aggregate stats, and manually retry failed deliveries.

<Warning>
  **One webhook per app per `service_type`.** Each developer app may have only one webhook for a given channel. Creating a second `whatsapp` webhook for the same app returns `400`. Delete the existing one first to free the slot.
</Warning>

***

## 2. Authentication & host

### Base URL

```
https://karibu.briq.tz
```

### Required headers

| Header         | Required         | Notes                                                                                                         |
| -------------- | ---------------- | ------------------------------------------------------------------------------------------------------------- |
| `X-API-Key`    | yes              | Your developer API key. Must be active and not expired. Missing/invalid -> **401**.                           |
| `Host`         | yes              | Must be `karibu.briq.tz`. Other hosts are rejected with **403** outside `development`/`sandbox` environments. |
| `Content-Type` | yes (POST/PATCH) | `application/json`                                                                                            |

All webhooks you create, read, or manage are scoped to **your** developer apps (resolved from the API key). A webhook owned by another user is invisible to you - reads return `404`.

### Response shape

These endpoints return **bare resource objects** (no `success`/`data`/`errors` envelope). Errors use the raw framework shape:

```json theme={null}
{ "detail": "Webhook not found" }
```

Treat any non-`2xx` status as a failure and read `detail` for the reason.

***

## 3. Endpoint reference

### 3.1 `POST /v1/webhooks/` - Create a webhook

Registers a callback URL for a developer app. The authenticated user must own the app referenced by `app_id`. A signing secret is generated automatically at creation; retrieve it separately via the [secret endpoint](#37-get-v1webhookswebhook_idsecret-reveal-signing-secret).

**Body:**

| Field          | Type               | Required | Description                                                                                                |
| -------------- | ------------------ | -------- | ---------------------------------------------------------------------------------------------------------- |
| `app_id`       | UUID               | yes      | ID of the developer app that will receive events. Must be an app you own.                                  |
| `service_type` | string             | yes      | Channel. One of `sms`, `voice`, `otp`, `whatsapp`, `email` (regex `^(sms\|voice\|otp\|whatsapp\|email)$`). |
| `url`          | string (HTTPS URL) | yes      | Public HTTPS URL we POST event payloads to. Must be reachable and return `2xx` on success.                 |

**Success - `201 Created`:**

```json theme={null}
{
  "webhook_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "app_id": "550e8400-e29b-41d4-a716-446655440000",
  "service_type": "whatsapp",
  "url": "https://api.yourdomain.com/briq/webhooks/whatsapp",
  "secret_token": "***",
  "created_at": "2026-03-15T12:00:00.000000",
  "updated_at": "2026-03-15T12:00:00.000000"
}
```

The returned `secret_token` is always masked as `***`. Fetch the real value with the secret endpoint.

| HTTP | Cause                                                                                       |
| ---- | ------------------------------------------------------------------------------------------- |
| 400  | App not found, not owned by you, or a webhook for this app + `service_type` already exists. |
| 422  | Invalid body (bad `service_type`, non-HTTPS/malformed `url`, non-UUID `app_id`).            |

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://karibu.briq.tz/v1/webhooks/" \
    -H "X-API-Key: YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "app_id": "550e8400-e29b-41d4-a716-446655440000",
      "service_type": "whatsapp",
      "url": "https://api.yourdomain.com/briq/webhooks/whatsapp"
    }'
  ```

  ```python Python theme={null}
  import requests

  resp = requests.post(
      "https://karibu.briq.tz/v1/webhooks/",
      headers={"X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json"},
      json={
          "app_id": "550e8400-e29b-41d4-a716-446655440000",
          "service_type": "whatsapp",
          "url": "https://api.yourdomain.com/briq/webhooks/whatsapp",
      },
      timeout=15,
  )
  print(resp.status_code, resp.json())
  ```

  ```javascript Node.js theme={null}
  const resp = await fetch("https://karibu.briq.tz/v1/webhooks/", {
    method: "POST",
    headers: { "X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json" },
    body: JSON.stringify({
      app_id: "550e8400-e29b-41d4-a716-446655440000",
      service_type: "whatsapp",
      url: "https://api.yourdomain.com/briq/webhooks/whatsapp",
    }),
  });
  console.log(resp.status, await resp.json());
  ```

  ```php PHP theme={null}
  <?php
  $ch = curl_init("https://karibu.briq.tz/v1/webhooks/");
  curl_setopt_array($ch, [
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_POST           => true,
      CURLOPT_HTTPHEADER     => ["X-API-Key: YOUR_API_KEY", "Content-Type: application/json"],
      CURLOPT_POSTFIELDS     => json_encode([
          "app_id"       => "550e8400-e29b-41d4-a716-446655440000",
          "service_type" => "whatsapp",
          "url"          => "https://api.yourdomain.com/briq/webhooks/whatsapp",
      ]),
  ]);
  echo curl_exec($ch);
  curl_close($ch);
  ```
</CodeGroup>

***

### 3.2 `GET /v1/webhooks/all` - List all webhooks for the user

Returns every webhook belonging to the authenticated user, across all developer apps and channels. Returns an empty array `[]` if you have none (not an error).

**Success - `200 OK`:** an array of webhook objects (same shape as the create response).

```bash theme={null}
curl "https://karibu.briq.tz/v1/webhooks/all" \
  -H "X-API-Key: YOUR_API_KEY"
```

***

### 3.3 `GET /v1/webhooks/app/{app_id}` - List webhooks for an app

Lists the webhooks registered for a single developer app you own. If the app is not yours or has no webhooks, an empty array `[]` is returned.

**Path parameter:** `app_id` (string, required).

```bash theme={null}
curl "https://karibu.briq.tz/v1/webhooks/app/550e8400-e29b-41d4-a716-446655440000" \
  -H "X-API-Key: YOUR_API_KEY"
```

***

### 3.4 `GET /v1/webhooks/{webhook_id}` - Get a webhook

Fetches a single webhook by ID. Must belong to one of your apps, otherwise `404`.

**Path parameter:** `webhook_id` (string, required).

**Success - `200 OK`:** a single webhook object.

| HTTP | Cause                                                                                   |
| ---- | --------------------------------------------------------------------------------------- |
| 404  | Webhook not found or not owned by one of your apps (`{"detail": "Webhook not found"}`). |

```bash theme={null}
curl "https://karibu.briq.tz/v1/webhooks/a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
  -H "X-API-Key: YOUR_API_KEY"
```

***

### 3.5 `PATCH /v1/webhooks/{webhook_id}` - Update a webhook

Updates a webhook's `url` and/or `service_type`. Both fields are optional; send only what you want to change.

**Body:**

| Field          | Type               | Required | Description                        |
| -------------- | ------------------ | -------- | ---------------------------------- |
| `service_type` | string             | no       | New channel (same enum as create). |
| `url`          | string (HTTPS URL) | no       | New public HTTPS callback URL.     |

<Note>
  The signing secret is fixed at creation and is **not** affected by an update. Existing receivers keep verifying `X-Briq-Signature` with the same secret.
</Note>

| HTTP | Cause                                                                   |
| ---- | ----------------------------------------------------------------------- |
| 404  | Webhook not found, not owned by one of your apps, or the update failed. |
| 422  | Invalid body (bad `service_type` or malformed/non-HTTPS `url`).         |

```bash theme={null}
curl -X PATCH "https://karibu.briq.tz/v1/webhooks/a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://api.yourdomain.com/briq/webhooks/whatsapp-v2" }'
```

***

### 3.6 `DELETE /v1/webhooks/{webhook_id}` - Delete a webhook

Permanently removes a webhook. After deletion, Briq stops sending events to its URL, and the per-`service_type` uniqueness slot is freed (you can create a new one for the same app + channel).

**Success - `204 No Content`** (no body).

| HTTP | Cause                                               |
| ---- | --------------------------------------------------- |
| 404  | Webhook not found or not owned by one of your apps. |

```bash theme={null}
curl -X DELETE "https://karibu.briq.tz/v1/webhooks/a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
  -H "X-API-Key: YOUR_API_KEY"
```

***

### 3.7 `GET /v1/webhooks/{webhook_id}/secret` - Reveal signing secret

Returns the webhook's signing secret in **plaintext**. Use it to verify the `X-Briq-Signature` header (HMAC-SHA256 of the raw request body) on incoming deliveries.

**Success - `200 OK`:**

```json theme={null}
{ "secret_token": "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }
```

| HTTP | Cause                                                                                                                                       |
| ---- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| 404  | Webhook not found / not owned (`{"detail": "Webhook not found"}`) **or** webhook has no secret set (`{"detail": "Webhook has no secret"}`). |

<Warning>
  Treat the secret as a credential. Store it in a secret manager or environment variable; never expose it client-side. The full HMAC verification procedure (header format, raw-body handling, sample code) is documented in the [WhatsApp reference -> Verifying the signature](/Karibu-WhatsApp/index#verifying-the-signature).
</Warning>

```bash theme={null}
curl "https://karibu.briq.tz/v1/webhooks/a1b2c3d4-e5f6-7890-abcd-ef1234567890/secret" \
  -H "X-API-Key: YOUR_API_KEY"
```

***

### 3.8 `POST /v1/webhooks/{webhook_id}/test` - Send a test event

Enqueues a synthetic test delivery so you can confirm your endpoint is reachable and your signature verification works. Returns an `event_id` you can use to find the delivery.

**Success - `202 Accepted`:**

```json theme={null}
{
  "event_id": "test_9f8e7d6c5b4a3210",
  "message": "Test event enqueued. Poll GET /{webhook_id}/deliveries to check status."
}
```

<Warning>
  **Synthetic `sms.sent` payload quirk.** The test endpoint currently emits a synthetic `sms.sent` event regardless of the webhook's `service_type`. For a `whatsapp` webhook this still verifies connectivity and signature verification, but the body is **not** a faithful WhatsApp event payload. A native `whatsapp.*` test event is **upcoming**.
</Warning>

```bash theme={null}
curl -X POST "https://karibu.briq.tz/v1/webhooks/a1b2c3d4-e5f6-7890-abcd-ef1234567890/test" \
  -H "X-API-Key: YOUR_API_KEY"
```

***

### 3.9 `GET /v1/webhooks/{webhook_id}/deliveries` - List delivery attempts

Returns a paginated, filterable list of delivery attempts for a webhook so you can audit what was sent, the HTTP status received, and any errors.

**Query parameters:**

| Field             | Type                | Required | Description                                           |
| ----------------- | ------------------- | -------- | ----------------------------------------------------- |
| `status`          | string              | no       | `pending`, `delivered`, `failed`, or `exhausted`.     |
| `event_name`      | string              | no       | Filter by event name (e.g. `briq.message.delivered`). |
| `from_created_at` | datetime (ISO 8601) | no       | Created on or after this timestamp.                   |
| `to_created_at`   | datetime (ISO 8601) | no       | Created on or before this timestamp.                  |
| `message_id`      | string              | no       | Filter by message ID.                                 |
| `limit`           | int                 | no       | Page size, `1`-`100`. Default `50`.                   |
| `offset`          | int                 | no       | Items to skip, `>= 0`. Default `0`.                   |

**Success - `200 OK`:**

```json theme={null}
{
  "items": [
    {
      "id": "d1e2f3a4-b5c6-7890-abcd-ef0123456789",
      "event_id": "evt_0a1b2c3d4e5f",
      "webhook_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "app_id": "550e8400-e29b-41d4-a716-446655440000",
      "service_type": "whatsapp",
      "event_name": "briq.message.delivered",
      "status": "failed",
      "attempt_count": 3,
      "max_attempts": 5,
      "last_status_code": 500,
      "last_error": "Internal Server Error",
      "next_attempt_at": "2026-03-15T12:10:00.000000",
      "created_at": "2026-03-15T12:00:00.000000",
      "updated_at": "2026-03-15T12:05:00.000000"
    }
  ],
  "total": 1,
  "limit": 20,
  "offset": 0
}
```

**Delivery summary fields**

| Field                       | Type             | Description                                                    |
| --------------------------- | ---------------- | -------------------------------------------------------------- |
| `id`                        | string           | Unique delivery id. Use it with the get-one-delivery endpoint. |
| `event_id`                  | string           | Event id from the envelope (idempotency).                      |
| `webhook_id`                | string           | Target webhook config id.                                      |
| `app_id`                    | string \| null   | Developer app id.                                              |
| `service_type`              | string           | Channel: `sms`, `email`, `voice`, `otp`, `whatsapp`.           |
| `event_name`                | string           | Event name.                                                    |
| `status`                    | string           | `pending` \| `delivered` \| `failed` \| `exhausted`.           |
| `attempt_count`             | int              | Number of delivery attempts made.                              |
| `max_attempts`              | int              | Maximum attempts configured.                                   |
| `last_status_code`          | int \| null      | HTTP status from the last attempt.                             |
| `last_error`                | string \| null   | Last error message if failed.                                  |
| `next_attempt_at`           | datetime \| null | When the next attempt is due.                                  |
| `created_at` / `updated_at` | datetime         | Timestamps.                                                    |

<Note>
  List items omit the `url` and `payload` fields. Use [get-one-delivery](#311-get-v1webhooksdeliveriesdelivery_id-get-one-delivery) for the full payload snapshot.
</Note>

```bash theme={null}
curl "https://karibu.briq.tz/v1/webhooks/a1b2c3d4-e5f6-7890-abcd-ef1234567890/deliveries?status=failed&limit=20" \
  -H "X-API-Key: YOUR_API_KEY"
```

***

### 3.10 `GET /v1/webhooks/{webhook_id}/stats` - Delivery stats

Returns aggregated delivery outcome counts over a time window, broken down by status and event name. Useful for monitoring health and spotting failure spikes.

**Query parameter:** `since` (datetime, ISO 8601, optional). Defaults to 7 days ago.

**Success - `200 OK`:**

```json theme={null}
{
  "webhook_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "since": "2026-03-01T00:00:00.000000",
  "total": 124,
  "by_status": { "delivered": 118, "failed": 4, "exhausted": 1, "pending": 1 },
  "by_event": { "briq.message.delivered": 100, "briq.message.failed": 24 }
}
```

```bash theme={null}
curl "https://karibu.briq.tz/v1/webhooks/a1b2c3d4-e5f6-7890-abcd-ef1234567890/stats?since=2026-03-01T00:00:00Z" \
  -H "X-API-Key: YOUR_API_KEY"
```

***

### 3.11 `GET /v1/webhooks/deliveries/{delivery_id}` - Get one delivery

Returns the full detail for a single delivery, including the target `url` and the `payload` snapshot of the JSON body that was sent (or is queued to send).

**Path parameter:** `delivery_id` (string, required) - the `id` from a delivery list item.

**Success - `200 OK`:** every delivery-summary field **plus**:

| Field     | Type           | Description                                   |
| --------- | -------------- | --------------------------------------------- |
| `url`     | string         | Callback URL this delivery targets.           |
| `payload` | object \| null | Snapshot of the JSON body sent or to be sent. |

| HTTP | Cause                                                |
| ---- | ---------------------------------------------------- |
| 404  | Delivery not found or not owned by one of your apps. |

```bash theme={null}
curl "https://karibu.briq.tz/v1/webhooks/deliveries/d1e2f3a4-b5c6-7890-abcd-ef0123456789" \
  -H "X-API-Key: YOUR_API_KEY"
```

***

### 3.12 `POST /v1/webhooks/deliveries/{delivery_id}/retry` - Retry a delivery

Schedules a previously failed delivery for another attempt. Only deliveries with status `failed` or `exhausted` can be retried.

**Success - `200 OK`:** the updated delivery detail (typically `status: "pending"` with `next_attempt_at` set).

| HTTP | Cause                                                                         |
| ---- | ----------------------------------------------------------------------------- |
| 404  | Delivery not found or not owned by one of your apps.                          |
| 422  | Delivery is not in a retryable state (status is not `failed` or `exhausted`). |

<Note>
  Retry requeues the **same** delivery row - it does not create a duplicate. The same `id` is updated and re-attempted.
</Note>

```bash theme={null}
curl -X POST "https://karibu.briq.tz/v1/webhooks/deliveries/d1e2f3a4-b5c6-7890-abcd-ef0123456789/retry" \
  -H "X-API-Key: YOUR_API_KEY"
```

***

## 4. What your endpoint must return

| Your HTTP response                      | Briq behavior                                                      |
| --------------------------------------- | ------------------------------------------------------------------ |
| Any **2xx**                             | Treated as **success**; Briq stops retrying this attempt.          |
| Non-2xx, timeout, or connection failure | Treated as **failure**; retried with backoff up to `max_attempts`. |

The response body is not interpreted for success - **204 No Content** or **200 OK** with an empty body is sufficient. Respond quickly (acknowledge, then process asynchronously) so your endpoint isn't marked as failing under load.

***

## 5. Related references

* [Karibu WhatsApp API](/Karibu-WhatsApp/index) - WhatsApp send/read endpoints and the `whatsapp.sent`/`delivered`/`read`/`failed` event payloads delivered to these webhooks.
* [Webhooks (subscriber guide)](/guides/webhooks) - what your HTTPS endpoint receives, signature verification, idempotency, and retry behavior.
* [Developer Apps](/guides/developer-apps) - API keys and app configuration.
