{"_id":"@cool-ai/beach-transport-whatsapp","_rev":"4-33eb754667b3bce1504a13feaf643cd5","name":"@cool-ai/beach-transport-whatsapp","dist-tags":{"latest":"0.2.2"},"versions":{"0.2.0":{"name":"@cool-ai/beach-transport-whatsapp","version":"0.2.0","license":"Apache-2.0","_id":"@cool-ai/beach-transport-whatsapp@0.2.0","maintainers":[{"name":"johncandrew","email":"johncandrew@yahoo.co.uk"}],"homepage":"https://cool-ai.org","bugs":{"url":"https://gitlab.com/johncandrew/beach/-/issues"},"dist":{"shasum":"765ccfe1e714d4d5589d9f515f280b6ff90cc627","tarball":"https://registry.npmjs.org/@cool-ai/beach-transport-whatsapp/-/beach-transport-whatsapp-0.2.0.tgz","fileCount":43,"integrity":"sha512-yNB/jW6/PuMM3A01sLxqgWUv8FuLwPu0/kX2gu1WzL0vRGhbf5b6TE965TBFJJ8TA1asTH8aMCGwq5my+DjkOw==","signatures":[{"sig":"MEYCIQCwdw4oZge09FZUQUXKXrUYNNU02jo7UvrrpXh19LgafgIhAKZ/uiEpNpuIddQtUItsAn7mGapN3X/f2+LyThLuV92K","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":144799},"type":"module","_from":"file:cool-ai-beach-transport-whatsapp-0.2.0.tgz","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"dev":"tsc --project tsconfig.json --watch","test":"vitest run","build":"tsc --project tsconfig.json","typecheck":"tsc --noEmit","test:watch":"vitest"},"_npmUser":{"name":"johncandrew","email":"johncandrew@yahoo.co.uk"},"_resolved":"/tmp/6a4d54460a135299f48f9e7334d70595/cool-ai-beach-transport-whatsapp-0.2.0.tgz","_integrity":"sha512-yNB/jW6/PuMM3A01sLxqgWUv8FuLwPu0/kX2gu1WzL0vRGhbf5b6TE965TBFJJ8TA1asTH8aMCGwq5my+DjkOw==","repository":{"url":"git+https://gitlab.com/johncandrew/beach.git","type":"git","directory":"packages/transport-whatsapp"},"_npmVersion":"10.9.7","description":"WhatsApp Cloud API transport adapters for Beach — webhook signature verification, payload parsing, and outbound /messages POST.","directories":{},"_nodeVersion":"22.22.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^2.0.0","typescript":"^5.5.0"},"_npmOperationalInternal":{"tmp":"tmp/beach-transport-whatsapp_0.2.0_1777476724593_0.9021995117650454","host":"s3://npm-registry-packages-npm-production"},"deprecated":"No longer published. Folded into @cool-ai/beach-channel-whatsapp."},"0.2.1":{"name":"@cool-ai/beach-transport-whatsapp","version":"0.2.1","license":"Apache-2.0","_id":"@cool-ai/beach-transport-whatsapp@0.2.1","maintainers":[{"name":"johncandrew","email":"johncandrew@yahoo.co.uk"}],"homepage":"https://cool-ai.org","bugs":{"url":"https://gitlab.com/johncandrew/beach/-/issues"},"dist":{"shasum":"7aa4b674fa976be0898b605f391f5aeaa4fb1c12","tarball":"https://registry.npmjs.org/@cool-ai/beach-transport-whatsapp/-/beach-transport-whatsapp-0.2.1.tgz","fileCount":43,"integrity":"sha512-Gcnta+CPptgwsOzMLY+arGrDfDemSeVwwQLlQCGYmNMFPeUTL0c2Mv/ntwPvcju0KPdJTFv6eEPNOq6LlAueyQ==","signatures":[{"sig":"MEUCIQDg/p48DJIHtqmc5B5/XvaFCrOHslnnuP0VKjiE+AM2TQIgc9h9kOSl875+/pT2MWUX/zzmFKy6sQ+r2MVDYNFlBE4=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":148511},"type":"module","_from":"file:cool-ai-beach-transport-whatsapp-0.2.1.tgz","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"dev":"tsc --project tsconfig.json --watch","test":"vitest run","build":"tsc --project tsconfig.json","typecheck":"tsc --noEmit","test:watch":"vitest"},"_npmUser":{"name":"johncandrew","email":"johncandrew@yahoo.co.uk"},"_resolved":"/tmp/d79dd81c55d94953faa100ed81f97634/cool-ai-beach-transport-whatsapp-0.2.1.tgz","_integrity":"sha512-Gcnta+CPptgwsOzMLY+arGrDfDemSeVwwQLlQCGYmNMFPeUTL0c2Mv/ntwPvcju0KPdJTFv6eEPNOq6LlAueyQ==","repository":{"url":"git+https://gitlab.com/johncandrew/beach.git","type":"git","directory":"packages/transport-whatsapp"},"_npmVersion":"10.9.7","description":"WhatsApp Cloud API transport adapters for Beach — webhook signature verification, payload parsing, and outbound /messages POST.","directories":{},"_nodeVersion":"22.22.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^2.0.0","typescript":"^5.5.0"},"_npmOperationalInternal":{"tmp":"tmp/beach-transport-whatsapp_0.2.1_1778003258012_0.042406356553310376","host":"s3://npm-registry-packages-npm-production"},"deprecated":"No longer published. Folded into @cool-ai/beach-channel-whatsapp."},"0.2.2":{"name":"@cool-ai/beach-transport-whatsapp","version":"0.2.2","license":"Apache-2.0","_id":"@cool-ai/beach-transport-whatsapp@0.2.2","maintainers":[{"name":"johncandrew","email":"johncandrew@yahoo.co.uk"}],"homepage":"https://cool-ai.org","bugs":{"url":"https://gitlab.com/johncandrew/beach/-/issues"},"dist":{"shasum":"53136da8202539a38cbfbf4077caaf9c188ffae2","tarball":"https://registry.npmjs.org/@cool-ai/beach-transport-whatsapp/-/beach-transport-whatsapp-0.2.2.tgz","fileCount":27,"integrity":"sha512-rFawM1Rt586uBwmWZBQGw9Ha3AiSZKKvsXi+6Kcy2+uZnXf2+GCdSQRIRsaVrRD0tAyofMr7A7dxgmH+TjTlqQ==","signatures":[{"sig":"MEQCID9TldxQN1AW6MSRRHy4YHTG171AmoRAxzJwQihbVgm0AiAQiNsDihBzxX3xESDrVgVP8iQrm/CSKvwI1pkwC3aCmw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":88890},"type":"module","_from":"file:cool-ai-beach-transport-whatsapp-0.2.2.tgz","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"dev":"tsc --project tsconfig.json --watch","test":"vitest run","build":"tsc --project tsconfig.json","typecheck":"tsc --noEmit","test:watch":"vitest"},"_npmUser":{"name":"johncandrew","email":"johncandrew@yahoo.co.uk"},"_resolved":"/tmp/151cf34ddbb838e19122f2a4b8c3ff2d/cool-ai-beach-transport-whatsapp-0.2.2.tgz","_integrity":"sha512-rFawM1Rt586uBwmWZBQGw9Ha3AiSZKKvsXi+6Kcy2+uZnXf2+GCdSQRIRsaVrRD0tAyofMr7A7dxgmH+TjTlqQ==","repository":{"url":"git+https://gitlab.com/johncandrew/beach.git","type":"git","directory":"packages/transport-whatsapp"},"_npmVersion":"10.9.7","description":"WhatsApp Cloud API transport adapters for Beach — webhook signature verification, payload parsing, and outbound /messages POST.","directories":{},"_nodeVersion":"22.22.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^2.0.0","typescript":"^5.5.0"},"_npmOperationalInternal":{"tmp":"tmp/beach-transport-whatsapp_0.2.2_1778191677337_0.7042918392180477","host":"s3://npm-registry-packages-npm-production"},"deprecated":"No longer published. Folded into @cool-ai/beach-channel-whatsapp."}},"time":{"created":"2026-04-29T15:32:04.479Z","modified":"2026-06-06T07:59:08.582Z","0.2.0":"2026-04-29T15:32:04.796Z","0.2.1":"2026-05-05T17:47:38.181Z","0.2.2":"2026-05-07T22:07:57.488Z"},"bugs":{"url":"https://gitlab.com/johncandrew/beach/-/issues"},"license":"Apache-2.0","homepage":"https://cool-ai.org","repository":{"url":"git+https://gitlab.com/johncandrew/beach.git","type":"git","directory":"packages/transport-whatsapp"},"description":"WhatsApp Cloud API transport adapters for Beach — webhook signature verification, payload parsing, and outbound /messages POST.","maintainers":[{"name":"johncandrew","email":"johncandrew@yahoo.co.uk"}],"readme":"# @cool-ai/beach-transport-whatsapp\n\n> **Home:** [cool-ai.org](https://cool-ai.org) · **Documentation:** [cool-ai.org/docs](https://cool-ai.org/docs)\n\nWhatsApp Cloud API transport adapters for Beach. Wire layer only — webhook signature verification, payload parsing, and outbound `/messages` POST. Channel-shaped concerns (envelope translation, the `Missive` part-bag, threading rules) belong above this in a future `@cool-ai/beach-channel-whatsapp` package; this one stops at the wire.\n\n## When to use this package directly\n\n- You are writing a custom WhatsApp channel on top of a different missive shape than Beach's stock one.\n- You want signature-verified inbound parsing without committing to the rest of Beach.\n- You need to swap the formatter on the outbound adapter without taking the channel package.\n\nIf none of those match, wait for the channel package — it will bundle these adapters with the canonical envelope-to-payload translation.\n\n## Quickstart\n\n```ts\nimport {\n  WhatsAppInboundAdapter,\n  WhatsAppOutboundAdapter,\n} from '@cool-ai/beach-transport-whatsapp';\nimport express from 'express';\n\nconst inbound = new WhatsAppInboundAdapter({\n  config: {\n    appSecret:   process.env.META_APP_SECRET!,\n    verifyToken: process.env.META_VERIFY_TOKEN!,\n  },\n  onMessage: async (parsed) => {\n    /* parsed is wire-shaped — translate to a Missive in your channel layer */\n  },\n});\n\nconst app = express();\n// IMPORTANT: do NOT mount express.json() ahead of the webhook — Meta signs\n// the raw bytes; a re-serialised body will not verify.\napp.all('/whatsapp/webhook', inbound.createWebhookHandler());\napp.listen(3000);\n\nconst outbound = new WhatsAppOutboundAdapter({\n  phoneNumberId: process.env.META_PHONE_NUMBER_ID!,\n  appSecret:     process.env.META_APP_SECRET!,\n  verifyToken:   process.env.META_VERIFY_TOKEN!,\n  tokenProvider: async () => process.env.META_BEARER_TOKEN!,\n});\n\nawait outbound.send({\n  messageType: 'text',\n  to: '447700900001',\n  body: 'Hello from Beach',\n});\n```\n\n## Inbound — `WhatsAppInboundAdapter`\n\n`createWebhookHandler()` returns a Node `http` request handler that the consumer mounts under their own routing, auth, and TLS termination — the same edge-only shape as `createInspectHandler`. The adapter does **not** bind itself to Express.\n\nThe handler answers two flavours of request:\n\n- **GET** — Meta's subscription handshake. When `hub.mode === 'subscribe'` and `hub.verify_token` matches the configured token, responds 200 with the value of `hub.challenge`. Otherwise 403.\n- **POST** — webhook delivery. Reads the raw body, verifies `X-Hub-Signature-256` against `appSecret` in constant time, parses each `messages[]` entry into a `ParsedInboundWhatsApp`, and invokes `onMessage` for each. Always responds 200 once the signature has verified — Meta retries 5xx aggressively, and the consumer is expected to handle idempotency on `messageId`.\n\nErrors thrown from `onMessage` are caught and logged; the webhook still responds 200. The transport's job is \"make sure Meta doesn't retry against verified content\"; recovery from a downstream failure is the consumer's.\n\n### Supported inbound content kinds\n\n`ParsedInboundWhatsApp.content` is a discriminated union covering text, image, video, audio (voice + non-voice), document, sticker, location, contacts, button-reply, list-reply, reaction. Unknown types are surfaced as `{ kind: 'unsupported', rawType }` rather than dropped — the consumer decides whether to ignore or log.\n\n## Outbound — `WhatsAppOutboundAdapter`\n\nPure transport. Pass it the wire-shaped `OutboundWhatsApp` payload and the adapter:\n\n1. Validates Meta's documented field limits (button counts, title lengths, list-row totals) and throws synchronously if violated.\n2. Awaits `tokenProvider()` so the bearer token is always current. **Beach does not cache or refresh tokens** — the provider is. The same callback shape is used by the SMTP OAuth2 adapter.\n3. POSTs to `https://graph.facebook.com/v<N>/<phoneNumberId>/messages` with `Authorization: Bearer <token>`.\n4. Returns `{ messageId }` (Meta's `wamid....`) on success.\n5. Throws `WhatsAppSendError` on non-2xx, carrying `status` and the response body so consumers can branch on transient vs permanent failures.\n\n### Supported outbound message types\n\n| `messageType`           | Meta API kind          | Notes |\n|-------------------------|------------------------|-------|\n| `text`                  | text                   | `body` 1..4096 chars; optional `previewUrl` |\n| `image`                 | image                  | `mediaId` xor `link`; optional caption |\n| `video`                 | video                  | `mediaId` xor `link`; optional caption |\n| `audio`                 | audio                  | `mediaId` xor `link` |\n| `document`              | document               | `mediaId` xor `link`; optional `filename`, `caption` |\n| `interactive-buttons`   | interactive (button)   | 1..3 buttons; titles 1..20 chars |\n| `interactive-list`      | interactive (list)     | ≤ 10 rows total; row titles ≤ 24; descriptions ≤ 72 |\n| `reaction`              | reaction               | `messageId` of parent + emoji (empty string clears) |\n\n`contextMessageId` on any of the above renders Meta's quote-reply UI.\n\n## What this package does not do\n\n- **No template messages.** WhatsApp's pre-approved template flow is a separate API surface; defer to a richer formatter when needed.\n- **No media upload.** Meta requires a `media_id` from a prior `/media` POST when sending `mediaId`-based messages. Upload is out of scope for v1; consumers either pass a public `link` or call Meta's `/media` endpoint themselves and pass the resulting id.\n- **No status callbacks** (`sent` / `delivered` / `read`). Meta delivers these in the same webhook payload alongside `messages[]`; this package surfaces only inbound user messages. Status handling lands as a follow-on if a consumer needs it.\n- **No rate limiting.** Meta enforces per-phone-number message rates. Consumers needing backpressure should wrap `send()` themselves until a shared rate-limit primitive lands.\n- **No Meta business-account management.** Phone numbers, template approvals, and webhook URL registration are consumer-side configuration in the Meta dashboard.\n- **No channel layer.** Translating a `ParsedInboundWhatsApp` into a Beach `Missive` (channelId, threadId, part-bag, the consumer's `onInbound`) is the channel package's job.\n\n## Testing\n\n- **Unit tests** in this repo cover signature verification, webhook parsing, the GET handshake, the POST flow (signed and unsigned), and outbound request shape for every supported message type. No network.\n- **Integration tests** against the real Meta API are left to the consumer. The Cloud API exposes test phone numbers for this purpose.\n\n## Related\n\n- [`beach-transport-email` README](../transport-email/README.md) — sibling transport package; shares the wire-vs-channel split pattern.\n- [`beach-channel-email` README](../channel-email/README.md) — what a channel layer on top of a transport looks like.\n","readmeFilename":"README.md"}