# WhatsApp API

Your app can send WhatsApp messages to a store's buyers through four endpoints. The messages use the order templates that Meta approved for DZBuild, and each one is paid from the store's WhatsApp wallet, exactly like the messages the WhatsApp Sender addon sends on its own. You cannot send free text or your own templates.

The full request and response reference is in the OpenAPI description, linked from the [API reference](https://dzbuild.dev/api-reference.md) page. This page explains how the pieces fit together.

## Scopes[​](#scopes "Direct link to Scopes")

| Scope           | Allows                                                                                |
| --------------- | ------------------------------------------------------------------------------------- |
| `whatsapp:read` | `GET /v1/whatsapp/templates`, `GET /v1/whatsapp/balance`, `GET /v1/whatsapp/messages` |
| `whatsapp:send` | `POST /v1/orders/{id}/whatsapp`                                                       |

Request only the scopes you need in the authorize URL. A call without the scope answers `403` with the code `forbidden` and the message `Missing scope: whatsapp:send` (or `whatsapp:read`). See [Scopes](https://dzbuild.dev/scopes.md).

## Templates[​](#templates "Direct link to Templates")

`GET /v1/whatsapp/templates` lists the six templates. It reads the approval status DZBuild keeps and never calls Meta.

| Key               | Sent for                                         |
| ----------------- | ------------------------------------------------ |
| `received`        | The order was received.                          |
| `confirmed`       | The order was confirmed.                         |
| `shipped_home`    | The parcel is on its way to the buyer's address. |
| `shipped_desk`    | The parcel is on its way to a courier desk.      |
| `delivery_failed` | The delivery attempt failed.                     |
| `desk_ready`      | The parcel is ready for pickup.                  |

Each item carries `key`, `name` (the template name at Meta, for example `dz_order_confirmed`), `toggle` (the merchant's matching switch in the addon) and `languages`. `languages.ar` and `languages.fr` each hold the `body` with its placeholders, an `example`, and the `status`. A template can only be sent in a language whose status is `APPROVED`. `UNKNOWN` means DZBuild has no status stored yet.

## Wallet[​](#wallet "Direct link to Wallet")

`GET /v1/whatsapp/balance` returns:

| Field          | Meaning                                                                                                                                             |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `balance`      | Messages left in the store's wallet. One message costs one credit.                                                                                  |
| `low_balance`  | `true` when fewer than 50 messages are left.                                                                                                        |
| `addon_active` | Whether the merchant has the WhatsApp Sender addon turned on.                                                                                       |
| `stats`        | Counts for the last 30 days by status (`sent`, `delivered`, `read`, `failed`) and `used_month`, the credits used since the first day of this month. |

The merchant tops up the wallet in the DZBuild dashboard. Your app cannot add credit.

## Send a message[​](#send-a-message "Direct link to Send a message")

`POST /v1/orders/{id}/whatsapp` queues one template for one order of the store. It is a write, so it needs an `Idempotency-Key` header (see [Rate limits](https://dzbuild.dev/rate-limits.md)).

```
POST /v1/orders/98765/whatsapp HTTP/1.1

Host: api.dzbuild.app

Authorization: Bearer dzpk_live_xxxxxxxx

Idempotency-Key: wa-98765-confirmed

Content-Type: application/json



{"template": "confirmed", "language": "fr"}
```

| Body field | Required | Meaning                                                                                                                                                                                                      |
| ---------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `template` | Yes      | One of the six keys, or `shipped`. `shipped` picks the right template from the order's delivery type: `shipped_home` for home delivery, `shipped_desk` for a desk, `desk_ready` for pickup.                  |
| `language` | No       | `ar` or `fr`. Without it, the language set in the store's WhatsApp Sender is used, or the store's language when the addon has none. When that fallback is neither `ar` nor `fr`, the message goes in Arabic. |

A manual send ignores the merchant's automatic switches in the addon. The addon itself must be active on the store.

The call answers `202` when the message is queued and the credit is taken:

```
{

  "data": { "message_id": 4411, "status": "queued", "template": "confirmed", "language": "fr" },

  "meta": { "request_id": "8f2c1a9d4b7e6035", "api_version": "v1" }

}
```

DZBuild sends queued messages in the background. Follow the result with `GET /v1/whatsapp/messages`.

## Answers and error codes[​](#answers-and-error-codes "Direct link to Answers and error codes")

| Status | Code                    | Meaning                                                                                                         |
| ------ | ----------------------- | --------------------------------------------------------------------------------------------------------------- |
| `202`  |                         | Queued, one credit taken.                                                                                       |
| `400`  | `bad_request`           | The order id is not numeric, or the body is not valid JSON.                                                     |
| `402`  | `no_credit`             | The store's wallet is empty. Nothing was queued and nothing was charged.                                        |
| `403`  | `addon_not_active`      | The merchant has not turned on the WhatsApp Sender addon.                                                       |
| `403`  | `forbidden`             | The token lacks `whatsapp:send`.                                                                                |
| `404`  | `not_found`             | No order with this id on this store.                                                                            |
| `409`  | `already_sent`          | This template was already sent for this order. The error object carries the existing message `id` and `status`. |
| `422`  | `unknown_template`      | `template` is not one of the accepted keys.                                                                     |
| `422`  | `invalid_language`      | `language` is not `ar` or `fr`.                                                                                 |
| `422`  | `invalid_number`        | The buyer's phone is not a valid Algerian mobile number.                                                        |
| `422`  | `suppressed`            | The buyer's number is blocked for WhatsApp messages on the platform.                                            |
| `422`  | `template_not_approved` | The template is not approved in that language.                                                                  |
| `422`  | `empty_param`           | A value the template needs is empty on the order.                                                               |
| `500`  | `send_failed`           | The message could not be queued. Retry later.                                                                   |

Each template can be sent once per order through the API. A later call for the same template and order answers `409`, even if the first message failed. The `422` answers from `invalid_number` to `empty_param` are different: nothing was charged, and the next call for that template and order tries again, so fix the cause (for example the phone number on the order) and call again with a new `Idempotency-Key`.

## Message log[​](#message-log "Direct link to Message log")

`GET /v1/whatsapp/messages` lists the store's messages, newest first. Query parameters: `order_id` to filter one order, `limit` (default 50, maximum 200) and `cursor` (the `next_cursor` of the previous page). The buyer's phone number is never returned.

| Field           | Meaning                                                                                                                                         |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`            | The message id, the same as `message_id` in the send answer.                                                                                    |
| `order_id`      | The order.                                                                                                                                      |
| `event`         | The template key for API messages, or the addon event (`received`, `confirmed`, `shipped`, `delivery_failed`, `desk_ready`) for automatic ones. |
| `source`        | `api` for messages sent through the API, `auto` for the addon's automatic messages.                                                             |
| `status`        | `queued`, `sending`, `sent`, `delivered`, `read`, `failed` or `skipped`.                                                                        |
| `language`      | `ar` or `fr`.                                                                                                                                   |
| `template_name` | The template name at Meta.                                                                                                                      |
| `error_title`   | Why the message failed or was skipped, or `null`.                                                                                               |
| `refunded`      | `true` when the credit was given back.                                                                                                          |
| `billing`       | `charged` once WhatsApp billed the message, `free` once the credit came back, `null` while WhatsApp has not reported the outcome.               |
| `created_at`    | When the message was queued.                                                                                                                    |

A message that fails, expires undelivered after 30 days, or is delivered without a charge from WhatsApp gets its credit back once: `refunded` turns `true` and `billing` becomes `free`. A message WhatsApp bills settles as `charged`.
