# Connect your app to WhaBuzz

> Let a WhaBuzz customer connect your app in one click. They approve on a WhaBuzz page; your server gets a scoped API key, a webhook secret and the number.

Canonical: https://docs.whabuzz.com/guides/connect-apps/

**Connect** lets a WhaBuzz customer hand your app access to one of their WhatsApp numbers without copying keys. It works like "Sign in with" flows: you send the user to a WhaBuzz page, they approve, WhaBuzz sends them back to you with a one-time code, and your server swaps the code for credentials.

Connect is available to apps registered with WhaBuzz. Today that is **WhaLeads** (`app=whaleads`). To register another app, write to support@whabuzz.com with its name and the host pattern of its callback.

## How it works {#flow}

1. Your app sends the user to `https://app.whabuzz.com/connect` with your app id, your callback URL and a random `state`.
2. The user signs in to WhaBuzz, picks a workspace and a number. If the workspace has no number yet, they connect one there through Meta (coexistence with the WhatsApp Business app included).
3. WhaBuzz shows what your app will be able to do and the **host** of your callback. The user presses **Approve**.
4. WhaBuzz sends a [verify ping](https://docs.whabuzz.com/guides/webhooks.md#verify-ping) to `https://<your host>/api/whabuzz/webhook`. If your server does not answer it, nothing is created and the user sees why.
5. WhaBuzz redirects the browser to your callback with `code` and `state`.
6. Your server posts the code to `https://api.whabuzz.com/connect/token` and receives the API key, the webhook signing secret and the number, once.

## 1. Send the user to WhaBuzz {#authorize}

```
https://app.whabuzz.com/connect
  ?app=whaleads
  &callback=https://acme.example.com/api/whabuzz/callback
  &state=Qm9vcC1iZWVwLTEyMzQ1Njc4OTA
  &code_challenge=VAVLIyYOqvtuLg_z2ITRlp_41lQmqEtYFeN-BpcepQ0
```

| Parameter | Required | Meaning |
|---|---|---|
| `app` | yes | Your registered app id. |
| `callback` | yes | `https://<your host>/api/whabuzz/callback` exactly: HTTPS, that path, no query string or fragment. Public host names only (no IP addresses or `localhost`). |
| `state` | yes | Up to 200 printable ASCII characters, no spaces. Make it random per attempt, keep it in the user's session, and compare it when the user comes back. |
| `code_challenge` | no | PKCE: base64url (no padding) of the SHA-256 of a random `code_verifier` of 43 to 128 characters. If you send it, the exchange needs the verifier. Recommended. |

Keep the whole link under about 500 characters: WhaBuzz carries it through sign-in, and a longer one makes the user start again after signing in.

## 2. Answer the verify ping {#verify}

Before anything is issued, WhaBuzz POSTs a signed ping to `https://<your host>/api/whabuzz/webhook`:

```json
{ "type": "webhook.verify", "challenge": "6f1c0b8e4a7d4f29b3e5a0c2d9f81a47", "endpoint_id": "3c2b…", "created_at": "2026-10-19T09:30:00.000Z" }
```

Answer `2xx` within 5 seconds with the `challenge` echoed, as the plain-text body or as JSON `{"challenge":"…"}`. You do not have the signing secret yet at this point, so accept the ping without checking `X-WhaBuzz-Signature` (it is still signed, with the secret you receive in step 4). Check signatures on everything else.

Your webhook receives all events (`*`) for the whole workspace, starting the moment your server completes the exchange in step 4. See [Webhooks](https://docs.whabuzz.com/guides/webhooks.md) for payloads, retries and the signature check.

## 3. Handle the callback {#callback}

On approve, the browser lands on:

```
https://acme.example.com/api/whabuzz/callback?code=7Hq0…(43 characters)&state=Qm9vcC1iZWVwLTEyMzQ1Njc4OTA
```

If the user pressed **Cancel**:

```
https://acme.example.com/api/whabuzz/callback?error=access_denied&state=Qm9vcC1iZWVwLTEyMzQ1Njc4OTA
```

Compare `state` with the one you stored before doing anything else. The code works once and expires after **5 minutes**.

## 4. Exchange the code {#token}

Server to server, never from the browser:

```bash
curl -X POST https://api.whabuzz.com/connect/token \
  -H "Content-Type: application/json" \
  -d '{"app":"whaleads","code":"7Hq0…","state":"Qm9vcC1iZWVwLTEyMzQ1Njc4OTA","code_verifier":"dBjftJeZ4CVP-mJ92K1RmuIBRHzyl1Ftw7gJA4KiBnI"}'
```

No `Authorization` header: the code is the credential. Send `code_verifier` only if you sent a `code_challenge`.

`200 OK`, shown once (the response carries `Cache-Control: no-store`):

```json
{
  "api_key": "wb_live_…",
  "webhook_secret": "whsec_…",
  "phone_number_id": "106540352242922",
  "waba_id": "102290129340398",
  "tenant_id": "a578246e-b97f-48e6-ae42-939ec3ccf917",
  "display_phone_number": "+91 98765 43210"
}
```

Store `api_key` and `webhook_secret` encrypted. WhaBuzz keeps only hashes and cannot show them again; if they are lost, the user connects again.

| Status | `error.code` | Meaning |
|---|---|---|
| `400` | `unknown_app` | `app` is not a registered app. |
| `400` | `invalid_code` | The code, `state` or `code_verifier` is wrong or malformed. A wrong `state` or verifier also burns the code. |
| `410` | `code_used` | The code was already exchanged. WhaBuzz treats this as a leaked code and **revokes** the connection it created. Ask the user to connect again. |
| `410` | `code_expired` | More than 5 minutes passed. Ask the user to connect again. |
| `410` | `connection_gone` | The user revoked or replaced the connection before you exchanged the code. |
| `409` | `connection_unavailable` | The number was disconnected or the webhook endpoint deleted since the user approved. |
| `429` | `rate_limited` | More than 10 exchanges a minute from your IP. Retry after `Retry-After` seconds. |

## What the key can do {#scopes}

The key is limited to the one number the user picked and to these scopes:

| Scope | Allows |
|---|---|
| `messages` | `POST /v1/messages`, `GET /v1/messages/…` |
| `templates` | `/v1/templates…`, for the number's WhatsApp Business Account only |
| `media` | `/v1/media…` |
| `numbers:read` | `GET /v1/numbers…` (only this number is listed) |
| `events:read` | `GET /v1/events…`, `GET /v1/stream` (workspace-wide) |

Anything else answers `403 insufficient_scope`: API keys, webhooks, billing, tenant settings, exports, other numbers. A request that names another number answers `404`. A connected app's key cannot be rotated (`409`); connect again instead.

## Revoking and reconnecting {#revoke}

The user sees every connection under **Settings, Connected apps** in the console and can revoke it. Revoking deletes the key and the webhook endpoint at once: your next API call answers `401`, and deliveries stop.

Connecting the same workspace to the same host again replaces the earlier connection: after the new exchange succeeds, the old key and endpoint are revoked. Use the newest credentials.

## Checklist {#checklist}

- `GET /api/whabuzz/callback`: check `state`, handle `error=access_denied`, exchange the code server-side within 5 minutes.
- `POST /api/whabuzz/webhook`: echo the verify `challenge`; verify `X-WhaBuzz-Signature` on every other request with `webhook_secret`.
- Never log `code`, `api_key` or `webhook_secret`.
- Treat `401` from the API as "the user revoked access": show a **Reconnect** button that starts step 1 again.
