# WhatsApp Cloud API webhook is active but no messages arrive: a read-only diagnostic

Your WhatsApp trigger (n8n WhatsApp Trigger, Make Watch Events, or your own endpoint) is active and the credential test passes, yet no messages show up. The break is usually in one of a few links between Meta and your receiver, and this script reads each link and prints which one is broken.

## How it works

`wa_webhook_doctor.py` makes read-only Graph API calls and prints PASS, FAIL, SKIP or MANUAL for each check, with fix text for failures:

1. `GET /{app-id}?fields=name`: the app id and app token are valid.
2. `GET /{app-id}/subscriptions`: finds the `whatsapp_business_account` object, compares `callback_url` with your expected production URL, checks `active` is true and `messages` is in `fields`. It flags an n8n `/webhook-test/` URL by name.
3. `GET /{waba-id}/subscribed_apps`: your app id is listed for the WhatsApp Business Account (WABA).
4. `GET /{waba-id}/phone_numbers`: your phone number id belongs to that WABA. Shows `display_phone_number` and `quality_rating` when present.
5. Callback probe: `GET <expected-callback-url>?hub.mode=subscribe&hub.verify_token=...&hub.challenge=12345` and checks the body is `12345`. Skipped without `--verify-token`.
6. and 7. Live mode, and messaging the real number instead of the test number. The script cannot read these, so it prints them as MANUAL reminders.

The only write action is `--apply`, which runs `POST /{waba-id}/subscribed_apps`, and only when check 3 says your app is missing. The script never deletes anything. It never calls the Meta endpoints that change the app-level callback, so it cannot repair a wrong callback_url (see limits).

The thread behind this (n8n community, 2026-11-04) shows why check 2 comes first: the person's manual test reported an existing app subscription, and a reply warned that n8n's failed-test cleanup can delete the app-level subscription. So do not run manual test executions on the trigger while debugging. Read the subscription instead.

## Setup

1. Make sure Python 3 is installed (`python3 --version`). No packages are needed.
2. Download `wa_webhook_doctor.py` into a folder. Keep `sample/` next to it if you want the offline demo.
3. Collect your ids from the Meta App Dashboard and WhatsApp Manager: App ID, WABA ID, and the Phone number ID (not the display number).
4. Get your production callback URL. In n8n, open the WhatsApp Trigger node and copy the **Production URL**, not the Test URL. In Make, copy the webhook address from the Watch Events webhook.
5. Set tokens as environment variables. Do not paste them into the command line or a file:
   ```
   export META_APP_TOKEN='APP_ID|APP_SECRET'
   export META_SYSTEM_TOKEN='<system user token with whatsapp_business_management>'
   ```
6. Run it (the verify token is optional and enables check 5; it is the one set in your Meta webhook configuration):
   ```
   python3 wa_webhook_doctor.py --app-id <APP_ID> --waba-id <WABA_ID> \
     --phone-number-id <PHONE_NUMBER_ID> \
     --expected-callback-url '<production URL>' --verify-token '<token>'
   ```
7. Read the first FAIL. It is the first broken link in the chain, and the FIX line says what to change.
8. If check 3 failed, re-run the same command with `--apply` to subscribe the app to the WABA. The system user token must belong to the same app as `--app-id`, because the call subscribes the app that owns the token.
9. If check 2 failed because the callback is a test URL or another URL, activate (publish) the workflow so the trigger re-registers, or correct the callback in the App Dashboard, then re-run the script.

The Graph version defaults to `v21.0`; change it with `--graph-version`. `--graph-base` changes the host. Check Meta's current docs for supported versions and exact response shapes.

## Test it offline

```
python3 test_fix.py
```

It runs the script against `sample/healthy/` and `sample/broken/` (fake ids, Sample Co, example.com) and checks the output. You can also run it by hand:

```
python3 wa_webhook_doctor.py --app-id 1000000000000001 --waba-id 2000000000000002 \
  --phone-number-id 3000000000000003 \
  --expected-callback-url https://n8n.example.com/webhook/sample-prod-id/webhook \
  --mock sample/broken --verify-token sample-token
```

Mock mode only replaces the HTTP function. The checks and printing are the same code as live mode. The real output is in `TEST_RESULTS.md`.

## Limits

- Not tested against the live Graph API. The endpoints and response shapes are from my knowledge of the Graph API, so check the current Meta docs if a call fails. Fixture shapes are examples, not captured responses.
- It reads only the first page of `/phone_numbers` and says so if a next page exists.
- It does not detect Live vs development mode or whether you texted the test number. Those are manual checks.
- It cannot rewrite the app-level callback URL. Do that in the App Dashboard, or by activating the workflow.
- `--apply` fixes only the WABA subscription. Re-subscribing does not fix a wrong callback or missing `messages` field.
- The callback probe sends your verify token to the URL you give it, so only point it at your own endpoint. The verify token is not printed. Whether an endpoint checks the token is up to that endpoint.
- Make's Watch Events manages the Meta-side subscription itself. Use this tool there to read state, and avoid editing the callback in Meta unless you know why.
