# Zalo ZNS

ZNS (Zalo Notification Service) sends a template message to a phone number through your Zalo Official Account (OA). The customer does not need to have messaged you first, which makes ZNS a good fit for order confirmations, payment reminders and delivery updates. You can send ZNS from an order page, to a filtered list of customers, from a workflow, or through the API. Zalo charges for each message.

## Before you start

1. **Connect a Zalo Official Account** under **Support → Channels**. See [Zalo OA](https://hatang.dev/help/channels/zalo-oa). The dashboard offers only accounts whose status is **Active**.
2. **Put the OA on a paid Zalo tier** that includes ZNS. Without it, Ha Tang cannot load your templates.
3. **Create templates and get them approved** in the Zalo OA admin: oa.zalo.me → ZNS → Templates. Ha Tang lists only approved templates.
4. **Keep the CRM (Customers) module on.** ZNS belongs to the Customers module: the API endpoints sit in that module, and queued sends are skipped while it is off. See [Businesses and modules](https://hatang.dev/help/getting-started/modules).

## The Zalo ZNS page

Open **Support → Channels**. The **Zalo ZNS** card has a **ZNS settings** button that opens the Zalo ZNS page. It shows, for the selected OA:

- **Quota today**: messages left today out of the OA's daily quota.
- Totals: sent, failed and queued messages.
- Approved templates with their name, id, status, quality and price per message.
- **Recent sends** with the time, phone number, template, status (and Zalo's error when it failed), and links to the order and customer.

If the business has several OAs, pick one at the top of the page.

## Send a notification for an order

1. Open the order (**Orders**, then the order).
2. On the **Zalo ZNS** card, click **Send notification**.
3. Pick the OA and the template. The phone number comes from the order's shipping address; it must be a Vietnamese mobile number.
4. Fill in the template parameters. Ha Tang pre-fills parameters whose names suggest the customer's name, the order code, the total or the phone number.
5. Click **Send ZNS**.

The message is sent right away. The card shows **Sent (msg …)** with Zalo's message id, or Zalo's error.

## Send to a list of customers

1. Open **Customers** and filter the list (search, stage, owner, label, channel).
2. Click **Send ZNS** (the same page is in the sidebar as **ZNS sending**).
3. Choose the OA and the template, and set **Max messages** (up to 5,000).
4. Map each **Template parameter** to a value:

   | Value | Meaning |
   |---|---|
   | `customer.name`, `customer.phone`, `customer.email` | The customer's details |
   | `field:<key>` | A custom field, for example `field:member_tier` |
   | `text:` followed by fixed text | The same text for everyone, for example `text:20/11` |

5. Check the count: the page shows how many customers match and how many have a phone number. Then click **Queue N ZNS messages**.

Customers without a phone number are skipped. Queued messages go out at the OA's pace, and the results appear under **Recent sends** on the Zalo ZNS page. Messages that hit Zalo's rate limit are retried, up to 3 attempts.

> **Caution**
>
> Zalo charges for every message. Check the number of recipients and the template price before you queue a large send.

## Send from a workflow

Add a **Send ZNS** step to a [workflow](https://hatang.dev/docs/guides/support/automation.md#workflows), for example on **An order is paid**. Pick the OA, enter the **ZNS template id** and the parameters, one `name=value` per line. Values can use `{{name}}`, `{{phone}}`, `{{email}}`, `{{order_code}}`, `{{order_total}}`, `{{rating}}` and `{{site}}`. The step uses the customer's phone number and is skipped when there is none.

## Send with the API

| Endpoint | Purpose |
|---|---|
| `GET /chat/zns/templates?account_id=cha_…` | Approved templates of an OA, with today's quota |
| `GET /chat/zns/templates/{id}?account_id=cha_…` | One template with its parameters |
| `POST /chat/zns` | Send one template message now |
| `POST /chat/zns/bulk` | Queue a template for every customer matching filters |
| `GET /chat/zns` | Messages sent by the business (`customer_id`, `order_id`, `limit` filters) |

Reads need the `crm:read` scope and sends need `crm:write`. Keys with `chat:read` / `chat:write` are accepted too. `account_id` is the id of the connected OA: the `cha_…` at the end of the OA's page address in the dashboard (`…/chat/channels/zalo/cha_…`).

### Send one message

Body fields:

| Field | Required | Notes |
|---|---|---|
| `account_id` | Yes | The Zalo OA account (`cha_…`) |
| `template_id` | Yes | An approved template id |
| `phone` | Yes | A Vietnamese mobile number; `0912 345 678`, `84912345678` and `+84 912 345 678` all work |
| `template_data` | Yes | Template parameters, as strings (keys up to 60 characters, values up to 500) |
| `customer_id`, `order_id` | No | Link the message to a customer and an order |
| `tracking_id` | No | Your own reference, up to 60 characters; defaults to the Ha Tang message id |

**cURL**

```bash
curl -X POST https://api.hatang.dev/chat/zns \
  -H "Authorization: Bearer ht_123..." \
  -H "Content-Type: application/json" \
  -d '{
    "account_id": "cha_123",
    "template_id": "231234",
    "phone": "0912345678",
    "template_data": { "customer_name": "Lan", "order_code": "DH1024", "amount": "350.000đ" },
    "order_id": "ord_123"
  }'
```

**Node.js**

```js
import { Hatang } from "@hatang/sdk";

const hatang = new Hatang(process.env.HATANG_API_KEY);

const { data, error } = await hatang.chat.zns.send({
  account_id: "cha_123",
  template_id: "231234",
  phone: "0912345678",
  template_data: { customer_name: "Lan", order_code: "DH1024", amount: "350.000đ" },
  order_id: "ord_123",
});
if (error) throw new Error(error.message);
if (data.status === "failed") console.error("Zalo refused:", data.error);
```

**Python**

```python
import os, requests

res = requests.post(
    "https://api.hatang.dev/chat/zns",
    headers={"Authorization": f"Bearer {os.environ['HATANG_API_KEY']}"},
    json={
        "account_id": "cha_123",
        "template_id": "231234",
        "phone": "0912345678",
        "template_data": {"customer_name": "Lan", "order_code": "DH1024", "amount": "350.000đ"},
        "order_id": "ord_123",
    },
)
message = res.json()
if message.get("status") == "failed":
    print("Zalo refused:", message["error"])
```

**PHP**

```php
<?php
$client = new GuzzleHttp\Client(['base_uri' => 'https://api.hatang.dev']);
$res = $client->post('/chat/zns', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('HATANG_API_KEY')],
    'json' => [
        'account_id' => 'cha_123',
        'template_id' => '231234',
        'phone' => '0912345678',
        'template_data' => ['customer_name' => 'Lan', 'order_code' => 'DH1024', 'amount' => '350.000đ'],
        'order_id' => 'ord_123',
    ],
]);
$message = json_decode($res->getBody(), true);
```

Response (`201 Created`):

```json
{
  "object": "chat_zns_message",
  "id": "zns_...",
  "account_id": "cha_123",
  "customer_id": null,
  "order_id": "ord_123",
  "phone": "84912345678",
  "template_id": "231234",
  "template_name": null,
  "template_data": { "customer_name": "Lan", "order_code": "DH1024", "amount": "350.000đ" },
  "tracking_id": null,
  "msg_id": "f1d2...",
  "status": "sent",
  "error": null,
  "created_at": "2026-10-23T03:00:00.000Z"
}
```

> **Note**
>
> The call returns `201` even when Zalo refuses the message. Check `status`: `sent` means Zalo accepted it (`msg_id` is Zalo's id); `failed` means Zalo refused it, and `error` holds Zalo's error code and message.

### Send to many customers

`POST /chat/zns/bulk` takes `account_id`, `template_id`, `mapping` (each template parameter mapped to `customer.name`, `customer.phone`, `customer.email`, `field:<key>` or `text:<fixed text>`), optional `filters` (`q`, `stage`, `owner_id`, `label`, `channel`) and `limit` (1 to 5,000, default 1,000). It responds `202 Accepted`:

```json
{ "object": "chat_zns_bulk", "queued": 418, "skipped": 12 }
```

`skipped` counts customers without a phone number or with a number that is not a valid Vietnamese mobile number.

### Webhook events

Each delivery attempt sends `chat.zns.sent` or `chat.zns.failed` with the message object above. See [Webhooks](https://hatang.dev/docs/reference/webhooks.md).

## Limits and quirks

- Phone numbers are normalized to the `84…` form. A number that does not end up with 11 or 12 digits is rejected with `422 validation_error`.
- Only approved templates can be sent. Templates with a status other than enabled do not appear in the list.
- The daily quota belongs to the OA and is set by Zalo. The Zalo ZNS page and `GET /chat/zns/templates` show the remaining quota.
- `account_id` must be a Zalo OA. A Zalo personal account cannot send ZNS.

## Troubleshooting

| What you see | Cause | Fix |
|---|---|---|
| "Could not load templates: … ZNS needs the OA on a paid tier with approved templates." | The OA is on a free tier, or its connection expired | Upgrade the OA, or reconnect it under **Support → Channels** |
| "No approved templates" | No template has been approved yet | Create templates at oa.zalo.me → ZNS → Templates and wait for approval |
| "Connect a Zalo Official Account first" | No active Zalo OA | Connect the OA, or reconnect it if it needs re-authorization |
| A send has status `failed` | Zalo refused it: wrong parameters, quota exhausted, or the phone number has no Zalo account | Read the `error`, fix the template data or wait for the quota to reset |
| Bulk messages stay queued | The Customers module is off | Turn the module back on |

## Next steps

- [Zalo OA](https://hatang.dev/help/channels/zalo-oa): connect the Official Account that sends ZNS.
- [Automation](https://hatang.dev/docs/guides/support/automation.md): send ZNS automatically when an order is paid.
- [Webhooks reference](https://hatang.dev/docs/reference/webhooks.md): track `chat.zns.sent` and `chat.zns.failed`.

Source: https://hatang.dev/docs/guides/support/zns
