npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cool-ai/beach-transport-whatsapp

v0.2.0

Published

WhatsApp Cloud API transport adapters for Beach — webhook signature verification, payload parsing, and outbound /messages POST.

Readme

@cool-ai/beach-transport-whatsapp

Home: cool-ai.org · Documentation: cool-ai.org/docs

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

When to use this package directly

  • You are writing a custom WhatsApp channel on top of a different missive shape than Beach's stock one.
  • You want signature-verified inbound parsing without committing to the rest of Beach.
  • You need to swap the formatter on the outbound adapter without taking the channel package.

If none of those match, wait for the channel package — it will bundle these adapters with the canonical envelope-to-payload translation.

Quickstart

import {
  WhatsAppInboundAdapter,
  WhatsAppOutboundAdapter,
} from '@cool-ai/beach-transport-whatsapp';
import express from 'express';

const inbound = new WhatsAppInboundAdapter({
  config: {
    appSecret:   process.env.META_APP_SECRET!,
    verifyToken: process.env.META_VERIFY_TOKEN!,
  },
  onMessage: async (parsed) => {
    /* parsed is wire-shaped — translate to a Missive in your channel layer */
  },
});

const app = express();
// IMPORTANT: do NOT mount express.json() ahead of the webhook — Meta signs
// the raw bytes; a re-serialised body will not verify.
app.all('/whatsapp/webhook', inbound.createWebhookHandler());
app.listen(3000);

const outbound = new WhatsAppOutboundAdapter({
  phoneNumberId: process.env.META_PHONE_NUMBER_ID!,
  appSecret:     process.env.META_APP_SECRET!,
  verifyToken:   process.env.META_VERIFY_TOKEN!,
  tokenProvider: async () => process.env.META_BEARER_TOKEN!,
});

await outbound.send({
  messageType: 'text',
  to: '447700900001',
  body: 'Hello from Beach',
});

Inbound — WhatsAppInboundAdapter

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 (CR-146). The adapter does not bind itself to Express.

The handler answers two flavours of request:

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

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

Supported inbound content kinds

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.

Outbound — WhatsAppOutboundAdapter

Pure transport. Pass it the wire-shaped OutboundWhatsApp payload and the adapter:

  1. Validates Meta's documented field limits (button counts, title lengths, list-row totals) and throws synchronously if violated.
  2. 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 SMTP OAuth2 (CR-130).
  3. POSTs to https://graph.facebook.com/v<N>/<phoneNumberId>/messages with Authorization: Bearer <token>.
  4. Returns { messageId } (Meta's wamid....) on success.
  5. Throws WhatsAppSendError on non-2xx, carrying status and the response body so consumers can branch on transient vs permanent failures.

Supported outbound message types

| messageType | Meta API kind | Notes | |-------------------------|------------------------|-------| | text | text | body 1..4096 chars; optional previewUrl | | image | image | mediaId xor link; optional caption | | video | video | mediaId xor link; optional caption | | audio | audio | mediaId xor link | | document | document | mediaId xor link; optional filename, caption | | interactive-buttons | interactive (button) | 1..3 buttons; titles 1..20 chars | | interactive-list | interactive (list) | ≤ 10 rows total; row titles ≤ 24; descriptions ≤ 72 | | reaction | reaction | messageId of parent + emoji (empty string clears) |

contextMessageId on any of the above renders Meta's quote-reply UI.

What this package does not do

  • No template messages. WhatsApp's pre-approved template flow is a separate API surface; defer to a richer formatter when needed.
  • 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.
  • 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.
  • No rate limiting. Meta enforces per-phone-number message rates. Consumers needing backpressure should wrap send() themselves until a shared rate-limit primitive (CR-139) lands.
  • No Meta business-account management. Phone numbers, template approvals, and webhook URL registration are consumer-side configuration in the Meta dashboard.
  • No channel layer. Translating a ParsedInboundWhatsApp into a Beach Missive (channelId, threadId, part-bag, the consumer's onInbound) is the channel package's job.

Testing

  • 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.
  • Integration tests against the real Meta API are left to the consumer. The Cloud API exposes test phone numbers for this purpose.

Related