# Message Forwarding to Your Own Endpoint

> Get each new WhatsApp message from your customers on your own server: set up message forwarding on a connection, check Avox's signature, and test it end to end.

Source: https://docs.theavoxai.com/integrations/webhooks

Turn on **Message forwarding** on a connection and Avox sends each new message your customers send to
that WhatsApp number to your own HTTPS endpoint, as it arrives, signed so you can check it came from
Avox. Use it to feed a CRM, a ticketing tool, your own bot or a data warehouse. Messages still appear
in the Avox [Inbox](https://docs.theavoxai.com/inbox) as usual.

Only incoming customer messages are forwarded - not delivery statuses, calls, messages your team
sends, or template and account events. Those stay on [Logs](https://docs.theavoxai.com/logs).

## Requirements

- A connected WhatsApp number.
- The **Admin** role in the Switchboard.
- An endpoint reachable from the public internet over `https://` on the standard port (443), with a
  valid TLS certificate. Private and local addresses are refused, and redirects aren't followed.
- Your endpoint can read the raw request body (to check the signature) and answer within 10 seconds.

## Setup

**1. Build the endpoint**

Add a `POST` route that checks the `Avox-Signature` header and answers `200` straight away. Copy a ready-made one for Express, Next.js, FastAPI or Flask from [Receive messages](https://docs.theavoxai.com/avox-api/webhooks#check-the-signature).

**2. Open the connection**

On [Connections](https://docs.theavoxai.com/connections), click **Settings** on the number and find **Message forwarding**.

**3. Enter the Endpoint URL**

Type your endpoint's `https://` address and click **Save URL**. If you entered a **Webhook URL** in the setup wizard, it's already filled in.

**4. Turn forwarding on**

Flip the switch. Copy the signing secret from **Your signing secret** - it's shown only once - and set it on your server, for example as `AVOX_WEBHOOK_SECRET`.

**5. Send a test event**

Click **Send test event**. You should see **Delivered - your endpoint answered 200**, and the test in **Recent deliveries**.

Every control in the section is described on [Message forwarding](https://docs.theavoxai.com/connections/webhook-override).

## What you receive

Each request is a `POST` with a `message.received` event:

```json
{
  "id": "evt_5f0c9a7e1b2d4c6e8f00a1b2c3d4e5f6",
  "type": "message.received",
  "api_version": "2026-10-01",
  "created_at": "2026-10-10T10:00:02.000Z",
  "connection_id": "0b1c2d3e-4f50-4a6b-8c7d-9e0f1a2b3c4d",
  "data": {
    "message_id": "wamid.HBgLMTU1NTEyMzQ1NjcVAgASGBQzQTdC",
    "from": "+15551234567",
    "contact_name": "Maya",
    "timestamp": "2026-10-10T10:00:00.000Z",
    "type": "text",
    "text": { "body": "Hi, is my order on its way?" }
  }
}
```

with the headers `Avox-Signature`, `Avox-Event-Id`, `Avox-Event-Type: message.received` and
`User-Agent: Avox-Webhooks/1.0`. Images, documents, locations, button taps and the other types are
listed with examples on [Receive messages](https://docs.theavoxai.com/avox-api/webhooks#whats-sent).

## A minimal receiver

```js
// Node.js 18+, Express - npm install express
import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";

const app = express();
const SECRET = process.env.AVOX_WEBHOOK_SECRET;

function verify(raw, header) {
  let t = null;
  const signatures = [];
  for (const part of (header ?? "").split(",")) {
    const [key, value = ""] = part.trim().split("=");
    if (key === "t" && /^\d+$/.test(value)) t = Number(value);
    if (key === "v1") signatures.push(value);
  }
  if (t === null || Math.abs(Date.now() / 1000 - t) > 300) return false; // 5 minutes
  const expected = createHmac("sha256", SECRET).update(`${t}.`).update(raw).digest();
  return signatures.some((sig) => {
    const given = Buffer.from(sig, "hex");
    return given.length === expected.length && timingSafeEqual(given, expected); // constant time
  });
}

app.post("/avox/messages", express.raw({ type: "application/json" }), (req, res) => {
  if (!verify(req.body, req.get("Avox-Signature"))) return res.sendStatus(401);
  res.sendStatus(200);
  const event = JSON.parse(req.body.toString("utf8"));
  // dedupe on event.id, skip event.test, then process event.data
});

app.listen(3000);
```

Versions for Next.js, FastAPI and Flask are on [Check the signature](https://docs.theavoxai.com/avox-api/webhooks#check-the-signature).

## Check it's working

- **Send test event** shows **Delivered**.
- Send a WhatsApp message to the number from your phone. Within a few seconds it reaches your endpoint,
  shows **Delivered** under **Recent deliveries**, and appears in the [Inbox](https://docs.theavoxai.com/inbox).
- Open **Payload** on a delivery to see exactly what was sent.
- Open **API logs** in the dashboard and choose the **Received** tab to see every forwarded message
  for all your numbers, with its delivery status, attempts and your endpoint's response code. See
  [API logs](https://docs.theavoxai.com/connections/api-keys#api-logs).

## Common errors

| You see | Fix |
| --- | --- |
| "Your endpoint answered 401" after **Send test event** ([AVX-3101](https://docs.theavoxai.com/error-codes/avx-3101)) | Check the signature over the raw body, with the whole `whsec_...` secret as the key. |
| "Your endpoint answered 301" or another redirect | Use the final `https://` address - redirects aren't followed. |
| "Your endpoint didn't answer within 10 seconds" | Answer `200` first, then do your processing. |
| "That host isn't reachable from the internet" | The URL points at a private or local address. Use a public host - for local testing, a tunnel with an `https://` address works. |
| "Enter your endpoint URL first" | Save an Endpoint URL before turning forwarding on. |
| **Paused** notice ([AVX-3104](https://docs.theavoxai.com/error-codes/avx-3104)) | 5 deliveries in a row failed after all retries. Fix the endpoint, send a test event, turn forwarding back on, and resend what you missed. |

More fixes are on [Troubleshooting](https://docs.theavoxai.com/avox-api/webhooks#troubleshooting).

## Security

- Always check the signature and the 5-minute timestamp window before trusting a request.
- Keep the signing secret in your server's secret store, never in code. It's shown once; nobody at
  Avox can see it afterwards.
- Rotate the secret if it may have leaked - the old one keeps working for 24 hours while you deploy
  the new one. See [Rotate the secret](https://docs.theavoxai.com/avox-api/webhooks#rotate-the-secret).
- Payloads carry only the message itself - no access tokens or account ids. Media is sent as Meta's
  media id, not the file.
- Delivery records are kept for 30 days, or your **Webhook Logs** retention on
  [Compliance](https://docs.theavoxai.com/compliance) if that's shorter.

## FAQ

**Does forwarding replace the Avox Inbox?**

No. Messages are stored and shown in Avox as always; forwarding sends a copy to your server.

**Are delivery statuses or my own sent messages forwarded?**

No. Only messages your customers send. Statuses are on [Logs](https://docs.theavoxai.com/logs); see
[Matching a status to a send](https://docs.theavoxai.com/avox-api/webhooks#matching-a-status-to-a-send).

**What happens if my server is down?**

Avox retries each message for about 21 hours. If 5 messages in a row still fail, forwarding pauses and
your Admins get an email. Nothing is lost from your Inbox, and you can resend from the delivery log.

**Can I change the URL later?**

Yes. An Admin edits the **Endpoint URL** and clicks **Save URL**. New deliveries go to the new address.

**What happens if I turn forwarding off?**

Nothing new is sent to your endpoint, and anything still waiting to go out is marked **Failed**
("Forwarding was turned off"). Messages keep arriving in your Inbox. After you turn forwarding back
on, an Admin can **Resend** any of them from the delivery log.

**Can a Manager set this up?**

A Manager can see the section, its state and the delivery log. Turning forwarding on or off,
changing the URL, rotating the secret, sending a test event and resending need the **Admin** role.
