> ## 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.

# Template-only Asia channels: the send playbook

> One consolidated playbook for WeChat, Zalo, and KakaoTalk template messaging — which Asia channels require approved templates, which accept free-form bodies, and which route through Meta messaging tags, with runnable sends, one inbound receiver, and campaign bulk send.

# Template-only Asia channels: the send playbook

Four of Orbit's Asia messaging channels — [WeChat](/channels/wechat), [Zalo](/channels/zalo), [KakaoTalk](/channels/kakao) Alimtalk, and anything routed through Meta ([Messenger](/channels/messenger), [Instagram](/channels/instagram)) — reject free-form text until it rides an approved template or a Meta `messaging_type` tag. [LINE](/channels/line) and [Telegram](/channels/telegram) accept a free-form `body`. Pick wrong and the send fails at validation with `422 VALIDATION_ERROR` before it ever reaches the provider.

This playbook is the one conceptual source the per-channel pages link back to: which channels demand a template, how the same request body moves across template channels with only the path swapped, what the 422 branch means, one receiver for all inbound traffic, and how to bulk-send templates through campaigns.

## 1. Which channels demand a template

| Channel (endpoint suffix)                                           | Send shape                                                                       | What it carries                                                                  |
| ------------------------------------------------------------------- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| [WeChat](/channels/wechat) (`/messages/wechat`)                     | Template-only                                                                    | Approved `template_name` + `template_params` filling `{{key.DATA}}` placeholders |
| [Zalo](/channels/zalo) (`/messages/zalo`)                           | Template-only                                                                    | Approved ZNS `template_name` + `template_params` filling named variables         |
| [KakaoTalk](/channels/kakao) (`/messages/kakao`)                    | Alimtalk: template-only; Friendtalk: free-form to followers                      | Selected per message with `metadata.kakao_message_type`                          |
| [LINE](/channels/line) (`/messages/line`)                           | Free-form                                                                        | Plain `body` + message `type` (text, image, video, flex, …)                      |
| [Telegram](/channels/telegram) (`/messages/telegram`)               | Free-form                                                                        | Plain `body`                                                                     |
| [Messenger](/channels/messenger) / [Instagram](/channels/instagram) | Free-form only inside Meta's 24-hour reply window; outside it, a tagged template | Top-level `messaging_type` / `tag` metadata selects the Meta messaging tag       |

<Note>
  For Messenger and Instagram, "template" is a Meta concept, not an Orbit pre-approval: outside the 24-hour customer-service window the send must carry a Meta messaging tag (for example `POST_UPDATE` or `ACCOUNT_UPDATE`) via the `messaging_type` metadata, and the recipient is only reachable if that tag's policy allows it. See the per-channel pages for the exact tag surface.
</Note>

## 2. Decision tree: one body, swap the channel

The template-only channels share the same request body shape. The only differences are the endpoint path and what `to` holds:

1. Recipient is a phone number (E.164 digits), template lives on Zalo? → `/messages/zalo`.
2. Recipient is a phone number, template lives on KakaoTalk, any recipient (follower or not)? → `/messages/kakao` with an approved Alimtalk code (via a campaign — see §5), or with `metadata.kakao_message_type: "friendtalk"` plus a `body` when the recipient added your channel as a friend.
3. Recipient is a WeChat `openid` captured from an inbound follow? → `/messages/wechat`.
4. You hold no approved template at all → you are on the wrong channel; switch to LINE or Telegram and send a free-form `body`, or get a template approved first.

Because the body never changes, a cross-channel sanity test is a single curl with only the path swapped:

```bash theme={null}
for ch in wechat zalo; do
  curl -s -X POST "https://api.orbit.devotel.io/api/v1/messages/${ch}" \
    -H "X-API-Key: ${ORBIT_API_KEY}" \
    -H "Content-Type: application/json" \
    -d '{
      "to": "84901234567",
      "template_name": "booking_confirmation",
      "template_params": { "name": "Linh", "code": "VN-4821" }
    }'
done
```

(WeChat needs an `openid` in `to`, so run its swap with a real follower id — the shape point stands: `to`, `template_name`, `template_params` is the whole contract for every template-only channel.)

KakaoTalk's free-form variant is the same endpoint with metadata selecting the type:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/messages/kakao \
  -H "X-API-Key: ${ORBIT_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "821012345678",
    "body": "Your order has shipped.",
    "metadata": { "kakao_message_type": "friendtalk" }
  }'
```

## 3. Why a template send returns 422

Template-only channels validate before the provider is contacted. Two distinct rejections:

* **`422 VALIDATION_ERROR` with no `template_name`** — ZNS and OA messaging are template-only, so a body-only send is refused at the API. This is the optimistic gate: the 422 carries no provider round trip, so it costs nothing and returns immediately.
* **`CHANNEL_NOT_CONFIGURED` (503)** — the channel has no connected credentials and no platform default; fix this first, template questions come after.

When the API-side check passes, the provider can still refuse the template: on WeChat and Zalo the response surfaces as `MESSAGE_SEND_FAILED` (502) with the upstream errcode — an unapproved or mistyped template name is the most common cause. Avoid it the same way the [WhatsApp template recipe](/guides/api-recipes) does: list your approved templates (`GET /api/v1/templates`), copy the exact approved name, and treat the approved-name cache as your source of truth rather than retyping names at each call site. (Meta's equivalent rejection is `WHATSAPP_TEMPLATE_NOT_FOUND` when the name or `language.code` does not match an approved template.)

The error branch, per channel — the same pattern the per-channel pages document:

```json theme={null}
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Zalo ZNS is template-only — an approved templateName (template_id) is required"
  }
}
```

```json theme={null}
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "KakaoTalk Alimtalk is template-only — an approved templateName (templateCode) is required"
  }
}
```

```json theme={null}
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "WeChat template message requires an approved templateName (template_id)"
  }
}
```

Fix on all three: pass an approved `template_name` (or, for KakaoTalk, either the approved Alimtalk code or a `friendtalk` body). The full per-channel error tables live on the [Zalo](/channels/zalo#common-errors), [WeChat](/channels/wechat#common-errors), and [KakaoTalk](/channels/kakao#common-errors) pages.

## 4. One receiver for all inbound traffic

Inbound replies from every Asia channel arrive on the same `message.received` webhook with the normalized envelope — subscribe once under **Settings → Webhooks** and route on `data.channel`. The full envelope shape is documented under [Normalized inbound event envelope](/webhooks/normalized-inbound-envelope); the receiver below is the whole handler for WeChat, Zalo, KakaoTalk, LINE, and Telegram.

<CodeGroup>
  ```javascript Node.js theme={null}
  import express from 'express';

  const app = express();
  app.use(express.json());

  app.post('/webhooks/orbit', (req, res) => {
    const event = req.body;
    if (event.type !== 'message.received') return res.sendStatus(200);

    const { channel, message_id, from, body } = event.data;
    switch (channel) {
      case 'wechat':
      case 'zalo':
      case 'kakao':
      case 'line':
      case 'telegram':
        // One queue for all Asia inbound; branch further only if you must.
        console.log(`inbound ${channel} from ${from}: ${body}`);
        break;
      default:
        console.log(`inbound on channel ${channel} — extend the switch as you add channels`);
    }
    res.sendStatus(200);
  });
  ```

  ```python Python theme={null}
  from flask import Flask, request, jsonify

  app = Flask(__name__)

  ASIA_CHANNELS = {"wechat", "zalo", "kakao", "line", "telegram"}

  @app.post("/webhooks/orbit")
  def handle():
      event = request.get_json(force=True)
      if event.get("type") != "message.received":
          return jsonify(ok=True), 200
      data = event["data"]
      if data.get("channel") in ASIA_CHANNELS:
          print(f"inbound {data['channel']} from {data.get('from')}: {data.get('body')}")
      return jsonify(ok=True), 200
  ```
</CodeGroup>

Store `data.from` on your contact record — on WeChat it is the follower's `openid`, and you need it as `to` for every outbound template send.

## 5. Bulk send via campaigns

Looping `POST /messages/<channel>` per recipient works, but the campaigns API is the intended bulk path and the only path for KakaoTalk Alimtalk (the direct endpoint only carries Friendtalk). Submit one campaign and the fan-out is one templated send per recipient, reusing the same `template_name` / `template_params` contract as the direct endpoints:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/campaigns \
  -H "X-API-Key: ${ORBIT_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Booking confirmations — ZNS",
    "channel": "zalo",
    "type": "blast",
    "audience_type": "all",
    "message_template": "booking_confirmation",
    "variables": { "name": "Linh", "code": "VN-4821" }
  }'
```

The campaign starts as a `draft`; launch it with `POST /api/v1/campaigns/{id}/send` and each recipient's delivery advances to `queued` from there. For WeChat and Zalo the same pattern holds — swap `channel` and the approved template name. Full parameter reference: [Campaigns API](/api-reference/endpoints/campaigns).

## 6. Per-channel references

This playbook covers the shared template contract once. For per-channel quirks — credential fields, rate limits, provider-specific error codes, KakaoTalk's Friendtalk/Alimtalk split, WeChat's H5 deep-link — go to the channel page:

* [Zalo](/channels/zalo) — ZNS templates, E.164 recipients
* [WeChat](/channels/wechat) — OA templates, `openid` recipients, optional `metadata.url` deep-link
* [KakaoTalk](/channels/kakao) — Alimtalk vs Friendtalk, message-type metadata
* [LINE](/channels/line) — free-form bodies, profile sender
* [Telegram](/channels/telegram) — free-form bodies

Run the onboarding order (credentials → webhook → first send) once per channel with the [APAC onboarding playbook](/guides/asia-channels-onboarding); use this page as the send-time contract reference that onboarding links to.
