|
| 1 | +# zaileys — Official Meta Cloud API provider (☁️) |
| 2 | + |
| 3 | +The cloud-exclusive reference. zaileys runs on **two providers** behind one API: |
| 4 | + |
| 5 | +- **🔗 unofficial (default)** — WhatsApp Web via Baileys. QR/pairing login, groups/channels/polls, |
| 6 | + full personal-account power. Documented in [api.md](api.md) + the other references. |
| 7 | +- **☁️ official** — the Meta WhatsApp **Cloud API**. Token auth, no ban risk, templates/OTP/ |
| 8 | + campaigns, Flows, commerce. **This file.** |
| 9 | + |
| 10 | +The `send(jid)…` builder and `on('text' | 'image' | …)` events are **identical** across providers |
| 11 | +(see [api.md](api.md)) — this file documents ONLY what's different or cloud-exclusive. Verified |
| 12 | +against zaileys **4.8.1** (`src/cloud/*`). |
| 13 | + |
| 14 | +## Switch provider |
| 15 | + |
| 16 | +```ts |
| 17 | +import { Client } from 'zaileys' |
| 18 | + |
| 19 | +const wa = new Client({ |
| 20 | + provider: 'cloud', // default is 'baileys' |
| 21 | + cloud: { |
| 22 | + accessToken: process.env.WA_TOKEN!, // permanent / system-user token (required) |
| 23 | + phoneNumberId: process.env.WA_PHONE_ID!, // sender phone-number id, NOT the number (required) |
| 24 | + wabaId: process.env.WA_WABA_ID, // WhatsApp Business Account id — needed for wa.cloud.* |
| 25 | + verifyToken: process.env.WA_VERIFY!, // webhook GET challenge secret you choose |
| 26 | + appSecret: process.env.WA_APP_SECRET!, // verifies webhook X-Hub-Signature-256 (recommended) |
| 27 | + apiVersion: 'v23.0', // optional, pinned default |
| 28 | + baseUrl: 'https://graph.facebook.com', // optional override |
| 29 | + }, |
| 30 | +}) |
| 31 | +``` |
| 32 | + |
| 33 | +`new Client()` (no `provider`) = baileys, unchanged. `client.provider` → `'baileys' | 'cloud'`. |
| 34 | +Constructing `provider:'cloud'` without `accessToken`+`phoneNumberId` throws `ZaileysCloudError('CONFIG')`. |
| 35 | + |
| 36 | +## connect() — no socket, no QR |
| 37 | + |
| 38 | +```ts |
| 39 | +await wa.connect() // lightweight Graph health-check, resolves immediately |
| 40 | +``` |
| 41 | + |
| 42 | +No QR, no pairing, no session file — the token authenticates. `qr`/`pairing-code`/reconnect events |
| 43 | +never fire on cloud. `disconnect()` tears down local listeners. |
| 44 | + |
| 45 | +**The webhook works even without `connect()`** — a serverless route (`export const POST = wa.webhook()`) |
| 46 | +never calls connect and still dispatches events (runtime is lazy-initialized). |
| 47 | + |
| 48 | +## Webhook — inbound is push-based |
| 49 | + |
| 50 | +`wa.webhook()` returns a framework-agnostic `(req: Request) => Promise<Response>`: |
| 51 | +GET → verification challenge (uses `verifyToken`); POST → verifies `X-Hub-Signature-256` HMAC (uses |
| 52 | +`appSecret`, rejects forgeries with 401), then dispatches events. Only available on cloud (throws on baileys). |
| 53 | + |
| 54 | +```ts |
| 55 | +// Next.js — app/api/whatsapp/route.ts |
| 56 | +const handler = wa.webhook() |
| 57 | +export const GET = handler |
| 58 | +export const POST = handler |
| 59 | + |
| 60 | +// Hono |
| 61 | +app.all('/webhook', (c) => handler(c.req.raw)) |
| 62 | + |
| 63 | +// Express — keep the RAW body (a body-parser breaks signature verification) |
| 64 | +app.all('/webhook', express.raw({ type: '*/*' }), async (req, res) => { |
| 65 | + const request = new Request(`${req.protocol}://${req.get('host')}${req.originalUrl}`, { |
| 66 | + method: req.method, |
| 67 | + headers: req.headers as Record<string, string>, |
| 68 | + ...(req.method === 'POST' ? { body: req.body as Buffer } : {}), |
| 69 | + }) |
| 70 | + const r = await handler(request) |
| 71 | + res.status(r.status).send(await r.text()) |
| 72 | +}) |
| 73 | +``` |
| 74 | + |
| 75 | +In the Meta dashboard: **WhatsApp → Configuration → Webhook** → set Callback URL + Verify token, |
| 76 | +**subscribe to the `messages` field**. Meta expects a 200 within ~10s or it retries — zaileys acks |
| 77 | +immediately, so make handlers **idempotent** (dedupe by message id). |
| 78 | + |
| 79 | +## Sending — same builder, cloud caveats |
| 80 | + |
| 81 | +All of `send(jid).text/image/video/audio/document/sticker/location/contact/buttons/list`, `reply`, |
| 82 | +`react`, `forward`, `markRead` work (see [api.md](api.md)). Cloud differences: |
| 83 | + |
| 84 | +- **Reply buttons max 3**; **carousels & polls not supported**; **media headers on buttons** unsupported. |
| 85 | +- **`markRead(messageId, { typing })`** — takes a message id (no chat cursor); optional typing indicator. |
| 86 | +- **Contacts need a first name** — vCard must have `FN:` + an `N:` line (zaileys derives it; error 131009 otherwise). |
| 87 | +- **AIRich (`rich: true`) is NOT supported** — it's a WhatsApp-Web-only proto. Throws a clear error; |
| 88 | + send plain text instead (WhatsApp renders `*bold*` `_italic_` `~strike~` `` ```mono``` `` natively). |
| 89 | +- **Media**: image ≤5MB, video ≤16MB, audio ≤16MB, document ≤100MB, sticker webp; URL must be public HTTPS or pass a Buffer/path. |
| 90 | + |
| 91 | +```ts |
| 92 | +await wa.send(to).text('*Order confirmed* ✅') |
| 93 | +await wa.send(to).image('https://cdn/x.jpg', { caption: 'hi' }) |
| 94 | +await wa.markRead(msg.chatId, { typing: true }) |
| 95 | +``` |
| 96 | + |
| 97 | +## The 24-hour window — the #1 gotcha |
| 98 | + |
| 99 | +Free-form sends (`text`/media/interactive) only work inside the **24h customer-service window** that |
| 100 | +opens when a user messages you. To a cold contact / after the window → **only approved templates**, |
| 101 | +else Graph error **131047**. |
| 102 | + |
| 103 | +```ts |
| 104 | +await wa.send('628xxx').text('hi') // ❌ 131047 if user never texted you |
| 105 | +await wa.sendTemplate('628xxx', 'welcome', 'en_US') // ✅ business-initiated |
| 106 | +``` |
| 107 | + |
| 108 | +## Templates — `sendTemplate` + `wa.cloud.templates` |
| 109 | + |
| 110 | +`sendTemplate(to, name, language, components?)` — takes the template **name**, not id. The number of |
| 111 | +`parameters` must exactly match the template's `{{n}}` or you get **132000**. |
| 112 | + |
| 113 | +```ts |
| 114 | +await wa.sendTemplate(to, 'promo_juli', 'id', [ |
| 115 | + { type: 'body', parameters: [{ type: 'text', text: 'Budi' }] }, |
| 116 | +]) |
| 117 | +// image header: |
| 118 | +await wa.sendTemplate(to, 'flash_sale', 'id', [ |
| 119 | + { type: 'header', parameters: [{ type: 'image', image: { link: 'https://…/banner.jpg' } }] }, |
| 120 | + { type: 'body', parameters: [{ type: 'text', text: '50%' }] }, |
| 121 | +]) |
| 122 | +``` |
| 123 | + |
| 124 | +**OTP** (AUTHENTICATION templates are usually approved instantly): |
| 125 | + |
| 126 | +```ts |
| 127 | +await wa.cloud.templates.create({ |
| 128 | + name: 'kode_otp', category: 'AUTHENTICATION', language: 'id', |
| 129 | + components: [ |
| 130 | + { type: 'BODY', add_security_recommendation: true }, |
| 131 | + { type: 'FOOTER', code_expiration_minutes: 5 }, |
| 132 | + { type: 'BUTTONS', buttons: [{ type: 'OTP', otp_type: 'COPY_CODE' }] }, |
| 133 | + ], |
| 134 | +}) |
| 135 | +await wa.sendTemplate(to, 'kode_otp', 'id', [ |
| 136 | + { type: 'body', parameters: [{ type: 'text', text: '839201' }] }, |
| 137 | + { type: 'button', sub_type: 'url', index: '0', parameters: [{ type: 'text', text: '839201' }] }, |
| 138 | +]) |
| 139 | +``` |
| 140 | + |
| 141 | +**Management** (needs `wabaId`): |
| 142 | + |
| 143 | +```ts |
| 144 | +await wa.cloud.templates.list({ status: 'APPROVED', limit: 100 }) |
| 145 | +await wa.cloud.templates.get('nama' | '1783414372642659') // by name OR numeric id → components |
| 146 | +await wa.cloud.templates.create({ name, category, language, components }) |
| 147 | +await wa.cloud.templates.delete('nama') |
| 148 | +wa.on('template-status', (t) => console.log(t.name, t.event)) // APPROVED / REJECTED / PAUSED |
| 149 | +``` |
| 150 | + |
| 151 | +## Marketing campaigns |
| 152 | + |
| 153 | +Create+approve template → broadcast → track. Marketing is subject to Meta's per-user daily cap + |
| 154 | +quality tiers (250 → unlimited by tier); watch `wa.cloud.info().quality_rating`. |
| 155 | + |
| 156 | +```ts |
| 157 | +for (const { phone, name } of contacts) { |
| 158 | + await wa.sendTemplate(phone, 'promo_juli', 'id', [{ type: 'body', parameters: [{ type: 'text', text: name }] }]) |
| 159 | +} |
| 160 | +wa.on('message-status', (s) => console.log(s.id, s.status)) // sent/delivered/read/failed |
| 161 | +const stats = await wa.cloud.analytics.messages({ start, end }) |
| 162 | +``` |
| 163 | + |
| 164 | +## `wa.cloud.*` — full management surface (needs `wabaId` where noted) |
| 165 | + |
| 166 | +```ts |
| 167 | +// account |
| 168 | +await wa.cloud.info() // display_phone_number, verified_name, quality_rating, throughput |
| 169 | +await wa.cloud.phoneNumbers() // all numbers on the WABA |
| 170 | + |
| 171 | +// business profile |
| 172 | +await wa.cloud.profile.get() |
| 173 | +await wa.cloud.profile.update({ about, address, description, email, websites, vertical }) |
| 174 | + |
| 175 | +// whatsapp flows |
| 176 | +await wa.cloud.flows.list() |
| 177 | +await wa.cloud.flows.send(to, { flowId, cta, bodyText, screen, flowToken?, data?, headerText?, footerText?, mode?, action? }) |
| 178 | +wa.on('flow-response', (f) => console.log(f.response)) // parsed nfm_reply |
| 179 | + |
| 180 | +// catalog & commerce |
| 181 | +await wa.cloud.commerce.catalogs() |
| 182 | +await wa.cloud.commerce.products(catalogId) |
| 183 | +await wa.cloud.commerce.sendProduct(to, { catalogId, retailerId, bodyText?, footerText? }) |
| 184 | +await wa.cloud.commerce.sendProductList(to, { catalogId, headerText, bodyText, sections: [{ title, productIds }] }) |
| 185 | +wa.on('order', (o) => console.log(o.items)) // { productRetailerId, quantity, price, currency }[] |
| 186 | + |
| 187 | +// address request (ID/BR only) |
| 188 | +await wa.cloud.sendAddressRequest(to, { bodyText, countryIso: 'ID' }) |
| 189 | + |
| 190 | +// blocklist / qr / analytics |
| 191 | +await wa.cloud.blocklist.add(['628xxx']); await wa.cloud.blocklist.remove(['628xxx']); await wa.cloud.blocklist.list() |
| 192 | +await wa.cloud.qr.create('prefilled msg', 'PNG'); await wa.cloud.qr.list(); await wa.cloud.qr.delete(code) |
| 193 | +await wa.cloud.analytics.conversations({ start, end, granularity: 'DAILY' }) |
| 194 | +await wa.cloud.analytics.messages({ start, end, granularity: 'DAY' }) |
| 195 | + |
| 196 | +// phone-number management (touches live registration — use with care) |
| 197 | +await wa.cloud.phone.register(pin); await wa.cloud.phone.deregister() |
| 198 | +await wa.cloud.phone.requestCode('SMS' | 'VOICE', 'id'); await wa.cloud.phone.verifyCode(code) |
| 199 | +``` |
| 200 | + |
| 201 | +`start`/`end` are Unix seconds. WABA-scoped calls throw `ZaileysCloudError('CONFIG')` without `wabaId`. |
| 202 | +Accessing `wa.cloud` on the baileys provider throws. |
| 203 | + |
| 204 | +## Events |
| 205 | + |
| 206 | +Shared events fire the same (see [api.md](api.md)). Cloud-only: |
| 207 | + |
| 208 | +| Event | Payload | |
| 209 | +| --- | --- | |
| 210 | +| `message-status` | `{ id, status: 'sent'\|'delivered'\|'read'\|'failed', recipientId, timestamp, error? }` | |
| 211 | +| `template-status` | `{ name, event, id, language?, reason? }` | |
| 212 | +| `flow-response` | `{ name, response, senderId, senderName?, id, timestamp }` | |
| 213 | +| `order` | `{ catalogId, items: {productRetailerId,quantity,price,currency}[], senderId, ... }` | |
| 214 | + |
| 215 | +## What's NOT on cloud → throws `UNSUPPORTED_ON_CLOUD` |
| 216 | + |
| 217 | +`group`, `community`, `newsletter`, `privacy`, `presence`, `chat`, `contact`, `business`, `profile` |
| 218 | +(the domain modules), plus `edit`, `delete`, `pin`, `setDisappearing`. Also: carousels, polls, |
| 219 | +AIRich, status/stories. All throw `ZaileysProviderError('UNSUPPORTED_ON_CLOUD')` immediately — never a |
| 220 | +silent no-op. For these, use the unofficial provider. |
| 221 | + |
| 222 | +## Cloud error codes (see [errors.md](errors.md)) |
| 223 | + |
| 224 | +`131047` re-engagement (outside 24h → use a template) · `132000` param count mismatch · `131009` |
| 225 | +contact name · `190` token expired (use a permanent System User token) · `131026` undeliverable · |
| 226 | +`131056` pair rate limit. All surface as `ZaileysCloudError` carrying the Graph code. |
0 commit comments