## Callback: whatsapp.connection

`POST <your callback URL>`

Something changed about the business's WhatsApp connection. `event` is one of `connected`,
`setup_failed`, `token_expiring` (now `degraded`), `recovered`, `paused`, `resumed`,
`suspended`, `revoked`, `disconnected`, `released` (an operator freed the disconnected
account; it no longer belongs to this business), or a Meta account or messaging-limit notice
(e.g. `ACCOUNT_VIOLATION`, `UPGRADE`). `status` is the account's state afterwards
(see ConnectionStatus).

### Headers

| Name | Description |
|---|---|
| `X-PaalChat-Event` |  |
| `X-PaalChat-Delivery` | Unique per event (equals body `id`). De-duplicate on it. |
| `X-PaalChat-Signature` | `t=<unix seconds>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>`. Reject if invalid or older than 300 seconds. |

### Body

| Field | Type | Description |
|---|---|---|
| `id` | string | Equals X-PaalChat-Delivery. |
| `sandbox` | boolean | true for sandbox businesses (test keys) - nothing reached WhatsApp. |
| `event` | any |  |
| `occurred_at` | string |  |
| `business` | object |  |
| `business.external_id` | string |  |
| `data` | object |  |
| `data.waba_id` | string |  |
| `data.event` | string | connected, setup_failed, token_expiring, recovered, paused, resumed, suspended, revoked, disconnected, released, or a Meta account/limit event name (e.g. ACCOUNT_VIOLATION, UPGRADE). |
| `data.status` | string | connected and degraded can send. pending = created or setup failed; connecting = signup in progress; degraded = working with a warning (token expiring, low quality); maintenance = paused by PaalChat operators (sends get 409 connection_paused); suspended = Meta disabled the account; revoked = the credential stopped working or access was removed at Meta; disconnected = disconnected on purpose. pending, revoked and disconnected need a new connect link. |

### Example

```json
{
  "id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f",
  "event": "whatsapp.connection",
  "occurred_at": "2026-09-30T18:40:02+00:00",
  "business": {"external_id": "presec"},
  "sandbox": false,
  "data": {"waba_id": "102290129340398", "event": "connected", "status": "connected"}
}
```

### Verify and handle (PHP)

```php
// routes/api.php: Route::post('/paalchat/callback', PaalChatCallbackController::class);
public function __invoke(Request $request)
{
    $header = (string) $request->header('X-PaalChat-Signature');
    $raw = $request->getContent();

    if (! preg_match('/^t=(\d+),v1=([a-f0-9]{64})$/', $header, $m)
        || abs(time() - (int) $m[1]) > 300
        || ! hash_equals(hash_hmac('sha256', $m[1].'.'.$raw, config('services.paalchat.callback_secret')), $m[2])) {
        abort(401);
    }

    // Process each delivery once.
    if (! Cache::add('paalchat:'.$request->header('X-PaalChat-Delivery'), true, now()->addDays(2))) {
        return response()->noContent();
    }

    $event = json_decode($raw, true);

    if ($event['event'] === 'whatsapp.connection') {
        Tenant::find($event['business']['external_id'])?->update(['whatsapp_status' => $event['data']['status']]);
    }

    return response()->noContent();
}
```
