WhatsApp messages
These four endpoints expose the WhatsApp Sender addon: the order message templates the platform has had approved, the store's WhatsApp wallet, the log of messages sent to buyers, and a call that sends one template to the buyer of one order. A message sent through the API follows the same rules as the automatic ones. It costs one message from the wallet, a message WhatsApp rejects is credited back, and a message that is never sent is never charged.
You cannot send free text. Every message is one of the six templates listed below, filled from the order (buyer first name, order number, store name, courier, pickup desk, amount to pay), in Arabic or French, with a track-my-order button.
Before you start
- The WhatsApp Sender addon must be active on the store (Add-ons page of the dashboard). The three read endpoints work without it; the send answers
403 addon_not_active. - The wallet is topped up from the addon's page in the dashboard (
/dashboard/whatsapp-sender, Top up). The API reads the balance but cannot top it up. - Messages go to Algerian mobile numbers only (05, 06 or 07). Any other number is skipped with
invalid_numberand costs nothing. - The key needs the WhatsApp scopes. New platform keys get both by default. Scopes are frozen when a key is created, so a key created before v1.6 does not have them: create a new key to use these endpoints.
| Scope | Description |
|---|---|
whatsapp:read | Read the WhatsApp order message templates, the WhatsApp wallet balance and the message log. Included in merchant keys created from v1.6 on; an older key needs a new key. |
whatsapp:send | Send WhatsApp order messages to buyers, each paid from the store's WhatsApp wallet. Included in merchant keys created from v1.6 on; an older key needs a new key. |
GET /v1/whatsapp/templates
The template catalogue: the Arabic and French text, example values for each placeholder, and the approval status of each language.
Auth: platform key with whatsapp:read.
The status is the last one the platform read from WhatsApp. It is refreshed at most every 10 minutes while messages are being sent, and UNKNOWN means it has not been read yet. This call never contacts WhatsApp itself. Only APPROVED templates are sent: a send in a language whose template is not approved is skipped with template_not_approved and costs nothing.
Request
curl 'https://api.dzbuild.app/v1/whatsapp/templates' \
-H "Authorization: Bearer $DZ_KEY"
Response 200
One of the six items is shown.
{
"data": {
"items": [
{
"key": "shipped_home",
"name": "dz_order_shipped_home",
"toggle": "shipped",
"languages": {
"ar": {
"body": "أهلاً {{1}}، طلبك رقم {{2}} من {{3}} في الطريق مع {{4}}.\nسيصلك خلال {{5}}. سيتصل بك عامل التوصيل قبل الوصول، يرجى إبقاء هاتفك متاحاً وتجهيز المبلغ: {{6}} دج.\nاضغط على الزر لتتبع طلبك.",
"example": ["أحمد", "1024", "متجري", "Yalidine", "يوم إلى 3 أيام", "3500"],
"status": "APPROVED"
},
"fr": {
"body": "Bonjour {{1}}, votre commande n° {{2}} chez {{3}} est en route avec {{4}}.\nLivraison prévue sous {{5}}. Le livreur vous appellera avant d'arriver : restez joignable et préparez le montant de {{6}} DA.\nAppuyez sur le bouton pour suivre votre commande.",
"example": ["Ahmed", "1024", "Ma Boutique", "Yalidine", "1 à 3 jours", "3500"],
"status": "APPROVED"
}
}
}
]
}
}
The six templates
key | toggle | What the buyer reads |
|---|---|---|
received | received | The order has arrived and the store will call to confirm it. |
confirmed | confirmed | The order is confirmed and being prepared, with the amount to pay. |
shipped_home | shipped | The order is on its way to the buyer's address, with the delivery delay set in the addon. |
shipped_desk | shipped | The order is on its way to a pickup desk, which the message names. |
delivery_failed | delivery_failed | The courier could not reach the buyer today and will try again tomorrow. |
desk_ready | desk_ready | The parcel is waiting at the pickup desk. |
toggle is the automatic-message switch in the addon settings that owns the template. It controls the automatic messages only; a send through the API ignores it.
GET /v1/whatsapp/balance
The wallet balance, the addon state and the message counts shown on the addon's page.
Auth: platform key with whatsapp:read.
Request
curl 'https://api.dzbuild.app/v1/whatsapp/balance' \
-H "Authorization: Bearer $DZ_KEY"
Response 200
{
"data": {
"balance": 412,
"low_balance": false,
"addon_active": true,
"stats": {
"sent": 12,
"delivered": 230,
"read": 158,
"failed": 4,
"used_month": 96
}
}
}
| Field | Meaning |
|---|---|
balance | Messages left in the wallet. A store that never topped up has 0. |
low_balance | true under 50 messages. |
addon_active | Whether the WhatsApp Sender addon is active on the store. |
stats.sent, stats.delivered, stats.read, stats.failed | Messages of the last 30 days, counted by their current status. A message the buyer read counts under read only. |
stats.used_month | Messages taken from the wallet since the 1st of the month, including the ones credited back later. |
GET /v1/whatsapp/messages
The store's messages, newest first: the automatic ones (source is auto) and the ones sent through the API (source is api). The buyer's phone number is never returned.
Auth: platform key with whatsapp:read.
Query parameters
| Param | Type | Default | Notes |
|---|---|---|---|
order_id | digits | none | Only the messages of this order. Anything other than digits returns 400 bad_request. |
limit | int | 50 | 1 to 200. |
cursor | string | none | next_cursor of the previous page. See Pagination. |
Request
curl 'https://api.dzbuild.app/v1/whatsapp/messages?order_id=6894' \
-H "Authorization: Bearer $DZ_KEY"
Response 200
{
"data": {
"items": [
{
"id": 4181,
"order_id": 6894,
"event": "shipped_home",
"source": "api",
"status": "read",
"language": "fr",
"template_name": "dz_order_shipped_home",
"error_title": null,
"refunded": false,
"created_at": "2026-09-26 10:14:03"
},
{
"id": 4180,
"order_id": 6894,
"event": "confirmed",
"source": "auto",
"status": "delivered",
"language": "ar",
"template_name": "dz_order_confirmed",
"error_title": null,
"refunded": false,
"created_at": "2026-09-25 18:02:41"
}
],
"next_cursor": null,
"has_more": false
}
}
| Field | Meaning |
|---|---|
event | For an automatic message, the order event that triggered it (received, confirmed, shipped, delivery_failed, desk_ready). For an API message, the template key that was sent. |
source | auto or api. |
status | See the table below. |
language | ar or fr. |
template_name | The template name registered with WhatsApp. |
error_title | Why a message was skipped or failed, otherwise null. |
refunded | true once the message of a failed send is back in the wallet. |
created_at | YYYY-MM-DD HH:MM:SS, server time. |
status | Meaning |
|---|---|
queued | Paid and waiting. The platform sends it within about a minute. |
sending | Being handed to WhatsApp. |
sent | Accepted by WhatsApp. |
delivered | Reached the buyer's phone. |
read | The buyer opened it. |
failed | WhatsApp could not deliver it. The message goes back to the wallet within a few minutes and refunded turns true. |
skipped | Never sent and never charged. error_title gives the reason. |
A skipped message carries one of these reasons in error_title:
error_title | Meaning |
|---|---|
invalid_number | The phone is not an Algerian mobile number. |
suppressed | The number has no WhatsApp account. |
template_not_approved | The template is still waiting for approval in that language. |
empty_param | The order is missing a value the template needs. |
POST /v1/orders/{id}/whatsapp
Queues one template for the buyer of one order and takes one message from the wallet. The platform sends it within about a minute.
Auth: platform key with whatsapp:send. Requires Idempotency-Key.
Body
| Field | Type | Required | Notes |
|---|---|---|---|
template | string | yes | A key from GET /v1/whatsapp/templates, or shipped, which picks shipped_home, shipped_desk or desk_ready from the order's delivery type (home, desk or pickup). |
language | ar or fr | no | Defaults to the message language chosen in the addon settings, or the store language when none was chosen. |
What the call does
- It checks that the addon is active and that the order belongs to the store. An order of another store answers
404, like an order that does not exist. - It ignores the addon's automatic-message switches: you can send a template the merchant turned off for automatic messages.
- Each template can be sent once per order through the API. A second call answers
409 already_sentwith the earlier message'sidandstatus. The one exception is an earlier attempt that was skipped, for example because the phone was invalid and has since been corrected on the order: then the call tries again. - It checks the phone, the template approval and the order data. A problem answers
422with the reason as the error code, records a skipped message and charges nothing. - It takes one message from the wallet. An empty wallet answers
402 no_creditand records nothing. - It answers
202. Follow the message withGET /v1/whatsapp/messages?order_id=and the order id. A message WhatsApp rejects turnsfailedand goes back to the wallet.
API messages are counted apart from the automatic ones. Sending shipped_home through the API does not stop the automatic on-its-way message for the same order, and the automatic message does not block the API send. Each is paid.
Request
curl -X POST 'https://api.dzbuild.app/v1/orders/6894/whatsapp' \
-H "Authorization: Bearer $DZ_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: wa-6894-shipped-1" \
-d '{"template": "shipped", "language": "fr"}'
Response 202
template is the key that was queued, with shipped already resolved.
{
"data": {
"message_id": 4181,
"status": "queued",
"template": "shipped_home",
"language": "fr"
}
}
Errors
| HTTP | Code | Cause |
|---|---|---|
| 400 | bad_request | The order id is not all digits, the body is not valid JSON, or Idempotency-Key is missing or malformed. |
| 402 | no_credit | The WhatsApp wallet is empty. Nothing was recorded. |
| 403 | forbidden | "Missing scope: whatsapp:send" |
| 403 | addon_not_active | The WhatsApp Sender addon is not active on the store. |
| 404 | not_found | No order with this id in the store. |
| 409 | already_sent | This template was already sent for this order through the API. |
| 422 | unknown_template | template is not shipped or a key from the catalogue. |
| 422 | invalid_language | language is not ar or fr. |
| 422 | invalid_number | The order's phone is not an Algerian mobile number. |
| 422 | suppressed | The order's phone has no WhatsApp account. |
| 422 | template_not_approved | The template is not approved yet in that language. |
| 422 | empty_param | The order is missing a value the template needs. |
| 422 | idempotency_key_reuse | The same Idempotency-Key was used with a different body. |
| 500 | send_failed | The message could not be queued. Retry with the same key. |
A 409 already_sent looks like this.
{
"error": {
"code": "already_sent",
"message": "This template was already sent for this order",
"id": 4181,
"status": "delivered"
}
}
Retries and Idempotency-Key
The first answer to a key is stored for 24 hours. A retry with the same key and the same body gets that answer back with Idempotency-Replay: 1, including a 402, 403 or 422. So after you top up the wallet, activate the addon or fix the order, retry with a new Idempotency-Key: the old key keeps returning the old error. A replayed 202 means no second message was queued.
The same key with a different body answers 422 idempotency_key_reuse. A 5xx or 429 answer is never stored, so retry it with the same key. If the message was in fact queued before the error, the retry answers 409 already_sent with its id, and nothing is charged twice. See Idempotency.
Known limits
- Delivery delay text.
shipped_homecarries the delivery delay the merchant typed in the addon settings, as typed. A French message from a store whose delay text is in Arabic shows that Arabic text inside the French message. A delay written with digits only, such as24-72h, reads the same in both languages. - Slow first send after an approval check. When the stored approval status of a template is older than 10 minutes, the send reads it again from WhatsApp before queuing. That happens at most once per template and language every 10 minutes and can hold the
POSTfor up to 15 seconds, so give your HTTP client a timeout of at least 20 seconds.