> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hookmyapp.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Development advice

> Build a WhatsApp integration that can see its own failures.

## 1. Log every failed call to a file

`hookmyapp channels logs` records what HookMyApp forwarded to your webhook URL and what your endpoint answered. It does not record the calls your app makes to us, so your app has to record those itself.

Already using pino, winston, or Sentry? Use it, and make sure four fields land in the record: HTTP `status`, the HookMyApp error `code`, the `x-request-id` response header, and which direction the call went.

Nothing in place yet? One function, no dependency:

```js theme={null}
// logs/hookmyapp.jsonl: one JSON line per HookMyApp call.
import { appendFileSync, mkdirSync } from 'node:fs';

mkdirSync('logs', { recursive: true });

export function logHookMyApp(entry) {
  const line = JSON.stringify({ ts: new Date().toISOString(), ...entry });
  console.log(line);
  appendFileSync('logs/hookmyapp.jsonl', line + '\n');
}
```

Call it on every non-2xx:

```js theme={null}
const res = await fetch(`${process.env.META_GRAPH_API_URL}/${phoneNumberId}/messages`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
  body: JSON.stringify(payload),
});

if (!res.ok) {
  const body = await res.text();
  logHookMyApp({
    dir: 'out',
    status: res.status,
    requestId: res.headers.get('x-request-id'),
    path: `/${phoneNumberId}/messages`,
    body: body.slice(0, 500),
  });
}
```

Write to a file, not only to stdout. A coding agent working in your project can read a file in the repo; it cannot read your hosting provider's log stream.

<Note>
  The gateway sets `x-request-id` on every response and honors one you send. A support ticket that names a request id is faster to answer.
</Note>

This file keeps growing. Nothing depends on old entries, so delete it whenever it gets large, or add it to `.gitignore` and clear it between runs.

## 2. Set your alert phone

```bash theme={null}
hookmyapp alerts phone set +14155552671
hookmyapp alerts phone verify 123456
```

HookMyApp texts this number when your integration breaks, so you find out without watching the logs.

## 3. Never retry a usage-limit rejection

A `429` with code `CHANNEL_USAGE_LIMIT_EXCEEDED` means your organization is over its plan allowance. Retrying cannot lift it and neither can waiting out the current period. Only an upgrade or a top-up does.

Every rejected call tells you so. The response body is:

```json theme={null}
{
  "statusCode": 429,
  "code": "CHANNEL_USAGE_LIMIT_EXCEEDED",
  "error": "channel_usage_limit_exceeded",
  "message": "This channel is over its usage limit. Upgrade your plan or add a top-up to resume.",
  "requestId": "01JQ8XZ4WK7N2M5RVT3PB9DGCA"
}
```

Log it, stop the work, and pass `message` straight through to whoever can act on it:

```js theme={null}
if (res.status === 429) {
  const err = JSON.parse(body);
  if (err.code === 'CHANNEL_USAGE_LIMIT_EXCEEDED') {
    logHookMyApp({ dir: 'out', status: 429, code: err.code, requestId: err.requestId, path });
    await notifyOwner(err.message);
    return;
  }
}
```

<Warning>
  Over the limit, your access token is refused for **every** call including read-only GETs, so the integration looks entirely dead. Surface `message` and the reason is obvious; swallow it and it looks like your app broke.
</Warning>

## 4. Keep the webhook route out of your auth middleware

HookMyApp is not a logged-in user of your app. If your webhook path sits behind session auth, an API-key gate, or a WAF rule, HookMyApp gets `401` or `403` and every inbound message is lost.

```js theme={null}
// Mount the webhook BEFORE the auth middleware, not after.
app.post('/webhooks/hookmyapp', express.json(), handleWebhook);
app.use(requireAuth);
```

Authenticate it with the HMAC signature instead: verify `X-HookMyApp-Signature-256` against `WEBHOOK_HMAC_SECRET`.

Answer fast, too. Return `200`, then do the work. A slow handler becomes a `504` on our side and the message is recorded as undelivered.

## 5. Use the CLI tunnel, not a hand-rolled one

An always-on self-hosted deployment is a supported pattern: a personal server, a Raspberry Pi, a long-running agent. `hookmyapp channels listen` is built for it, with a per-channel access-controlled tunnel on a stable hostname.

A hand-rolled tunnel is not. Free tunnel services hand out a new random hostname on every restart, so the URL stored by `webhook set` stops working the moment the tunnel restarts, with nothing to announce it.

Use `channels listen` for a self-hosted or local destination, and a real HTTPS URL for a deployed backend.

## Health check when something breaks

```bash theme={null}
hookmyapp channels health <channel>       # channel state and quality
hookmyapp channels logs list <channel>    # did HookMyApp deliver, and what answered
hookmyapp notifications                   # has HookMyApp already diagnosed this
```

Then read your own log from step 1 for the calls your app made.

| What you see                       | What it means                                                 | Next step                                                                   |
| ---------------------------------- | ------------------------------------------------------------- | --------------------------------------------------------------------------- |
| `502` or `504`                     | Your app is down, cold-starting, too slow, or the tunnel died | Check the destination is up; if it is a tunnel, confirm it is still running |
| `401` or `403`                     | Your own middleware is rejecting the webhook route            | Move that route outside the auth middleware (step 4)                        |
| `404`                              | The saved webhook URL does not match a route your app serves  | Compare `channels webhook show` against your actual route                   |
| Deliveries exist, none delivered   | The integration has not worked yet                            | Verify the destination URL and that the app is deployed                     |
| `429 CHANNEL_USAGE_LIMIT_EXCEEDED` | Over your plan allowance                                      | Upgrade or top up. Do not retry (step 3)                                    |
| Nothing at all                     | Forwarding is off, or nothing inbound arrived                 | `channels show <channel>` and look for `forwarding: enabled`                |
