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

# Zalo onboarding: connect your Official Account and send ZNS templates

> Walk the Zalo channel end to end — provision a Zalo Official Account and ZNS access token, send template-based notifications through the Messaging API, and receive inbound replies and delivery statuses on the standard webhooks.

# Zalo onboarding: connect your Official Account and send ZNS templates

This guide takes a Zalo channel from nothing to a working production integration: Official Account provisioning, the template-only send, inbound replies, and delivery statuses. The field and error reference lives on the [Zalo channel page](/channels/zalo) — this guide is the ordered path you run the first time. For the region-wide survey of LINE, WeChat, KakaoTalk, and Zalo, see [APAC channels onboarding](/guides/asia-channels-onboarding).

Zalo is a **beta** channel. The send and receive paths are wired end to end, but the Official Account, template approvals, and the ZNS access token are provisioned off-Orbit in the [Zalo for Developers](https://developers.zalo.me/) console.

## 1. Prerequisites: Official Account and consent

1. Create a **Zalo Official Account (OA)** in the Zalo for Developers console. The OA is the sender identity on every ZNS notification.
2. Register your **ZNS templates** in the same console and wait for Zalo approval. Each approved template receives a template id — that id is what `template_name` references on a send. An edited template is a new approval.
3. Generate the OA **access token** (OAuth `oauth/access_token` grant) and paste it in Orbit under **Settings → Channels → Zalo**. The token is short-lived; rotate it through Zalo's grant before it expires. Orbit stores it encrypted at rest and never echoes it back through any API response.

Consent alignment: ZNS notifications are numbers the end user shared with your business. Record consent (and the opt-out signal from the OA subscription list) in your own contact model before the first send, and route failures into your [suppression and opt-out list](/guides/message-suppression) so the next campaign skips opted-out recipients. [Tenant-owned controls](/guides/compliance-profiles-assemble) — quiet hours, consent, suppression — apply to Zalo exactly as to SMS; the template approval above is the only extra gate this channel adds.

Until credentials are connected, every send fails closed with `CHANNEL_NOT_CONFIGURED` (503).

## 2. Template-based send — no free-form body

ZNS is **template-only**: a business sends a pre-approved notification template to a customer's phone number. The send body on `POST /api/v1/messages/zalo` carries `to` and `template_name`, plus an optional flat `template_params` map filling the template's named variables. There is no free-form `body` field — a send without an approved `template_name` is rejected with `VALIDATION_ERROR` (422).

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/messages/zalo \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "to": "84901234567",
    "template_name": "booking_confirmation",
    "template_params": { "name": "Linh", "code": "VN-4821" },
    "metadata": { "order_id": "VN-4821" }
  }'
```

`202 Accepted` — the message is queued; the terminal `delivered` / `failed` state arrives later on the delivery-status webhook. Carry your own correlation key in `metadata` — it is echoed back on those events.

The sender is resolved automatically from the OA credentials connected to your organization; you never pass an access token on the request. The direct endpoint is capped at 80 requests/minute per organization — stage bulk template sends through the [Campaigns API](/api-reference/endpoints/campaigns) with `channel: "zalo"`, where `message_template` and `variables` map to `template_name` and `template_params`.

## 3. Samples: curl, Node.js, Python

<Tabs>
  <Tab title="curl">
    ```bash theme={null}
    curl -X POST https://api.orbit.devotel.io/api/v1/messages/zalo \
      -H "X-API-Key: dv_live_sk_..." \
      -H "Content-Type: application/json" \
      -d '{
        "to": "84901234567",
        "template_name": "booking_confirmation",
        "template_params": { "name": "Linh", "code": "VN-4821" }
      }'
    ```
  </Tab>

  <Tab title="Node.js">
    ```typescript theme={null}
    import { Orbit } from "@devotel-orbit/node";

    const orbit = new Orbit({ apiKey: process.env.ORBIT_API_KEY });

    // The Node SDK has no typed sendZalo method yet — reach the route
    // through the generic request() escape hatch.
    const { data } = await orbit.request("POST", "/messages/zalo", {
      to: "84901234567",
      template_name: "booking_confirmation",
      template_params: { name: "Linh", code: "VN-4821" },
    });

    console.log(data.id);     // msg_9f2c1e...
    console.log(data.status); // 'queued'
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    from orbit_sdk import OrbitClient

    client = OrbitClient.from_env()  # reads ORBIT_API_KEY

    # No typed Zalo helper either — the escape hatch takes a json_body.
    res = client.request(
        "POST",
        "/messages/zalo",
        json_body={
            "to": "84901234567",
            "template_name": "booking_confirmation",
            "template_params": {"name": "Linh", "code": "VN-4821"},
        },
    )
    print(res["data"]["id"])      # msg_9f2c1e...
    print(res["data"]["status"])  # 'queued'
    ```
  </Tab>
</Tabs>

## 4. Inbound replies and delivery statuses

There is no Zalo webhook to register. Inbound replies and ZNS delivery callbacks land on your account's standard webhooks — subscribe under **Settings → Webhooks**:

* **Inbound replies** arrive on `message.received` with `channel: "zalo"`, folded onto the [normalized inbound event envelope](/webhooks/normalized-inbound-envelope). Branch on `data.normalized.kind === "inbound"` instead of parsing event strings.
* **Delivery status** advances the outbound message to `delivered` / `failed`, reconciled by the tracking id Orbit mints per send. Your `metadata` map rides along on the event.

```typescript theme={null}
app.post('/webhooks/orbit', (req, res) => {
  // Verify the X-Orbit-Signature header first — see the signature guide below.
  const event = req.body;
  if (event.type === 'message.received' && event.data.channel === 'zalo') {
    if (event.data.normalized.kind === 'inbound') {
      console.log(`Zalo reply from ${event.data.from}: ${event.data.body}`);
    }
  }
  res.sendStatus(200); // Orbit keeps retrying until you 2xx.
});
```

Verify the `X-Orbit-Signature` header before trusting any event — [Verify webhook signatures](/guides/verify-webhook-signatures).

## 5. Common errors

| Code                     | HTTP | Cause                                                              | Fix                                                                                |
| ------------------------ | ---- | ------------------------------------------------------------------ | ---------------------------------------------------------------------------------- |
| `INVALID_RECIPIENT`      | 422  | `to` is missing or empty.                                          | Supply a recipient phone number in E.164 digits.                                   |
| `VALIDATION_ERROR`       | 422  | The send has no `template_name` — ZNS is template-only.            | Provide an approved ZNS template id.                                               |
| `CHANNEL_NOT_CONFIGURED` | 503  | No OA credentials connected and no platform default set.           | Paste the access token under **Settings → Channels → Zalo**.                       |
| `MESSAGE_SEND_FAILED`    | 502  | ZNS rejected the send (expired token, unapproved template, quota). | Read the ZNS error code in the message; refresh the token or correct the template. |
| `RATE_LIMITED`           | 429  | More than 80 sends/minute for your organization.                   | Honour `Retry-After`; move bulk sends to the Campaigns API.                        |

## See also

* [Zalo channel page](/channels/zalo) — field reference, error codes, and credential handling.
* [APAC channels onboarding](/guides/asia-channels-onboarding) — LINE, WeChat, KakaoTalk, and Zalo in one survey.
* [Template-only Asia channels playbook](/guides/asia-channels-template-playbook) — one approved template across every template channel.
* [Campaigns API](/api-reference/endpoints/campaigns) — bulk template sends.
* [Message suppression](/guides/message-suppression) — per-recipient opt-out handling.
