# WhatsApp

> WhatsApp Business voice calls with WhatsAppClient, over SmallWebRTC. Handles Meta's calling webhooks, signature checks, and accepting, rejecting and ending calls.

Source: https://polvallverdu.github.io/pipedog/docs/transports/whatsapp/



## Overview [#overview]

A user calls your WhatsApp Business number. Meta's Cloud API sends your webhook a `connect` event
with an SDP offer. `WhatsAppClient` verifies the request, answers the offer over a
[SmallWebRTC](https://polvallverdu.github.io/pipedog/docs/transports/smallwebrtc.md) connection (it **pre-accepts**, then **accepts** the call
through the Graph API), and hands you the connection. You run a pipeline on it with
`SmallWebRTCTransport`, exactly as for a browser client. This is a port of Pipecat's
`pipecat.transports.whatsapp`.

* **`WhatsAppClient`**: handles webhooks and calls, and keeps track of the ongoing calls.
* **`WhatsAppApi`**: the Graph API calls: answer (`pre_accept`, `accept`), `reject` and `terminate`.
* **`whatsAppWebhookHandler`**: a fetch handler for the webhook endpoint (`GET` verification and
  `POST` events). New in pipedog; upstream leaves this to your web framework.
* **`parseWebhookRequest`**, **`verifyWebhookRequest`**, **`verifyWebhookSignature`**: the pieces on
  their own.

<Cards>
  <Card title="Upstream page" href="https://docs.pipecat.ai/api-reference/server/services/transport/whatsapp">
    Pipecat's documentation for the same transport.
  </Card>

  <Card title="Source" href="https://github.com/polvallverdu/pipedog/tree/main/packages/transport-whatsapp/src">
    `client.ts`, `api.ts` and `http.ts`.
  </Card>

  <Card title="Meta's calling docs" href="https://developers.facebook.com/docs/whatsapp/cloud-api/calling/user-initiated-calls">
    The WhatsApp Business Calling API: webhooks and call actions.
  </Card>
</Cards>

## Installation [#installation]

```bash
bun add @pipedog/transport-whatsapp @pipedog/transport-smallwebrtc
```

```ts
import { WhatsAppClient, whatsAppWebhookHandler } from "@pipedog/transport-whatsapp";
```

## Runtimes [#runtimes]

| Runtime            | Works     | What it needs                                                                                                                                                                                                 |
| ------------------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Bun                | ✅ yes     | A WhatsApp Business account with Calling enabled, an access token and a public HTTPS webhook URL. A long-lived server that can use UDP (SmallWebRTC runs on werift).                                          |
| Node.js            | ✅ yes     | A WhatsApp Business account with Calling enabled, an access token and a public HTTPS webhook URL. A long-lived server that can use UDP (SmallWebRTC runs on werift).                                          |
| Deno               | ✅ yes     | A WhatsApp Business account with Calling enabled, an access token and a public HTTPS webhook URL. A long-lived server that can use UDP (SmallWebRTC runs on werift).                                          |
| Cloudflare Workers | ⚠️ partly | A WhatsApp Business account with Calling enabled, an access token and a public HTTPS webhook URL. Webhook verification, signature checks and API calls only. There is no WebRTC, so calls cannot be answered. |

Generated from `pipecat.runtimes` and `pipecat.compat` in `packages/transport-whatsapp/package.json`: ✅ means the compat smoke test passes on that runtime.

Answering calls needs WebRTC, which runs on werift and needs UDP sockets: Bun, Node and Deno only.
On **Cloudflare Workers** only the webhook parts work: Meta's `GET` verification, the
`X-Hub-Signature-256` check (HMAC through WebCrypto) and the Graph API calls (`rejectCallToWhatsapp`,
`terminateCallToWhatsapp`). A `connect` event there fails with an error that says Workers have no
WebRTC. The package's `./connection` export resolves to a stub under the `"workerd"` condition, so
werift is never bundled into a Worker.

## Prerequisites [#prerequisites]

* A WhatsApp Business phone number with **Calling** enabled, and a webhook subscribed to the `calls`
  field. See [Meta's calling docs](https://developers.facebook.com/docs/whatsapp/cloud-api/calling/user-initiated-calls).
* A system-user **access token** (`whatsappToken`) and the **phone number ID** (`phoneNumberId`).
* Your Meta **app secret** (`whatsappSecret`), so every webhook `POST` can be authenticated.
* A **verify token** you choose, entered in the Meta dashboard, for the webhook's `GET` check.
* A public HTTPS URL for the webhook, and a server that can use UDP for the media.

## Usage [#usage]

```ts
import { TransportParams } from "@pipedog/core/transports/base_transport";
import { SmallWebRTCTransport } from "@pipedog/transport-smallwebrtc/transport";
import { WhatsAppClient, whatsAppWebhookHandler } from "@pipedog/transport-whatsapp";

const client = new WhatsAppClient({
  whatsappToken: process.env.WHATSAPP_TOKEN!,
  phoneNumberId: process.env.WHATSAPP_PHONE_NUMBER_ID!,
  whatsappSecret: process.env.WHATSAPP_APP_SECRET, // checks X-Hub-Signature-256
});

const webhook = whatsAppWebhookHandler(client, {
  verifyToken: process.env.WHATSAPP_VERIFY_TOKEN!,
  connectionCallback: async (connection, call) => {
    const transport = new SmallWebRTCTransport(
      connection,
      new TransportParams({ audioInEnabled: true, audioOutEnabled: true }),
    );
    void runBot(transport, call.from); // your pipeline
  },
});

Bun.serve({ port: 8080, routes: { "/webhook": { GET: webhook, POST: webhook } } });
```

The callback gets the `SmallWebRTCConnection` and the connect event, with the caller's number
(`call.from`) and the call ID (`call.id`). If it throws, the call stays up and the error is logged.

### With the runner [#with-the-runner]

`@pipedog/runner` wires all of this for you, as upstream's runner does. Install
`@pipedog/transport-whatsapp`, set `WHATSAPP_APP_SECRET`, `WHATSAPP_PHONE_NUMBER_ID`,
`WHATSAPP_TOKEN` and `WHATSAPP_WEBHOOK_VERIFICATION_TOKEN`, and run:

```bash
bun bot.ts -t webrtc --whatsapp
```

It serves `GET /whatsapp` (Meta's verification) and `POST /whatsapp` (call events). Each call starts
your `bot(runnerArgs)` with `SmallWebRTCRunnerArguments`: build the transport with
`createTransport()` as for any WebRTC session, and read the caller from `runnerArgs.body`
(`from`, `id`). See the [runner](https://polvallverdu.github.io/pipedog/docs/runner/#whatsapp).

### Without the helper [#without-the-helper]

```ts
// GET: Meta checks the endpoint
const challenge = await client.handleVerifyWebhookRequest(params, verifyToken); // the number to send back

// POST: an event. Pass the raw body: the signature is over the exact bytes.
const raw = await request.text();
await client.handleWebhookRequest(parseWebhookRequest(JSON.parse(raw)), {
  connectionCallback,
  rawBody: raw,
  sha256Signature: request.headers.get("x-hub-signature-256"),
});
```

With a `whatsappSecret`, a request without a matching signature is rejected. Without one nothing is
checked, as upstream does.

## Configuration [#configuration]

`new WhatsAppClient(options)`:

| Option             | Type                                    | Default                             | Description                                     |
| ------------------ | --------------------------------------- | ----------------------------------- | ----------------------------------------------- |
| `whatsappToken`    | `string`                                | required                            | Graph API access token.                         |
| `phoneNumberId`    | `string`                                | required                            | The business phone number ID.                   |
| `whatsappSecret`   | `string \| null`                        | `null`                              | The app secret, to validate webhook signatures. |
| `iceServers`       | `string[] \| IceServer[]`               | Google's STUN server                | ICE servers for the calls' connections.         |
| `fetch`            | `typeof fetch`                          | the global `fetch`                  | Replaces upstream's aiohttp session.            |
| `baseUrl`          | `string`                                | `https://graph.facebook.com/v23.0/` | The Graph API URL.                              |
| `createConnection` | `(iceServers) => SmallWebRTCConnection` | a `SmallWebRTCConnection`           | Create the call's connection yourself.          |

Methods: `handleWebhookRequest`, `handleVerifyWebhookRequest`, `terminateAllCalls`, and the setters
`updateIceServers`, `updateWhatsappSecret`, `updateWhatsappToken` and `updateWhatsappPhoneNumberId`.
`terminateAllCalls()` ends every active call through the API and disconnects its connection.

### How a call is answered [#how-a-call-is-answered]

1. The `connect` event arrives. The client creates a `SmallWebRTCConnection` and gives it the offer.
2. It takes the SDP answer and removes every DTLS fingerprint but SHA-256, the only one WhatsApp accepts.
3. It sends `pre_accept`, then `accept`, with that answer. Meta allows 30 to 60 seconds from the
   webhook. If either fails, the connection is disconnected and the error is raised.
4. It calls your `connectionCallback`. The call ends on a `terminate` event, or when the connection closes.

## Differences from upstream [#differences-from-upstream]

* **`fetch` replaces `aiohttp.ClientSession`.** Every request goes through the `fetch` option.
* **The models are interfaces with the wire's names**, validated by `parseWebhookRequest`: `call.from`
  is upstream's `from_`, and the others stay `sdp_type`, `start_time` and so on. It also accepts the
  optional contact fields Meta's current docs describe.
* **The signature check is asynchronous**, because HMAC uses WebCrypto.
* The deprecated one-argument `connectionCallback(connection)` form is not ported.
* `updateWhatsappPhoneNumberId` also updates the calls URL, where upstream only stores the value.
