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

# Receiving webhooks

> Answer deliveries, check they came from ReachHunt, and handle retries

Your server gets each event as an HTTPS `POST` with a JSON body. This page covers how to answer it, check that it came from ReachHunt, and handle retries.

## Answer quickly

Answer with any `2xx` status within 10 seconds. Redirects are not followed: a `3xx` counts as a failure.

1. Answer with `2xx` first, then do the work.
2. Skip an `id` you already handled. The same event can arrive more than once, for example when your server worked but answered too late.
3. Compare `createdAt` when order matters. Events can arrive out of order, for example when one is retried.

## Check that a request came from ReachHunt

ReachHunt doesn't sign requests. Add a secret header to the webhook instead, then reject any request that doesn't carry it.

**1. Add a secret header**

In [**Settings → Webhooks**](https://app.reachhunt.ai/settings/webhooks), select **Add a secret header** when you add or edit a webhook. For example, use `X-Webhook-Secret` with a long random value.

**2. Check it on your server**

Compare the header with the value you saved, using a constant-time comparison. Answer `401` when it doesn't match.

The header is sent with every request, including retries and test events. Keep its value only on your server.

## Example handler

```js Node.js theme={"system"}
import crypto from 'node:crypto';
import express from 'express';

const app = express();
const secret = Buffer.from(process.env.REACHHUNT_WEBHOOK_SECRET);
const handled = new Set();

const fromReachHunt = (value = '') => {
  const received = Buffer.from(value);
  return received.length === secret.length && crypto.timingSafeEqual(received, secret);
};

app.post('/webhooks/reachhunt', express.json(), (req, res) => {
  if (!fromReachHunt(req.get('X-Webhook-Secret'))) return res.sendStatus(401);
  res.sendStatus(200);

  const event = req.body;
  if (handled.has(event.id)) return;
  handled.add(event.id);

  if (event.type === 'message.received') {
    console.log('New message:', event.data.message.text);
  }
});

app.listen(3000);
```

```python Python theme={"system"}
import hmac
import os

from flask import Flask, request

app = Flask(__name__)
SECRET = os.environ["REACHHUNT_WEBHOOK_SECRET"]
handled = set()


@app.post("/webhooks/reachhunt")
def reachhunt_webhook():
    if not hmac.compare_digest(request.headers.get("X-Webhook-Secret", ""), SECRET):
        return "", 401

    event = request.get_json()
    if event["id"] not in handled:
        handled.add(event["id"])
        if event["type"] == "message.received":
            print("New message:", event["data"]["message"]["text"])

    return "", 200
```

In production, store handled IDs in your database, and hand slow work to a queue so you still answer within 10 seconds.

## Retries

ReachHunt tries a delivery again when your server:

* does not answer within 10 seconds, or cannot be reached
* answers `408`, `429` or any `5xx`

It retries 6 times, after 1 minute, 5 minutes, 30 minutes, 1 hour, 6 hours and 24 hours. Any other answer, such as `400` or `401`, fails the delivery at once, without retries.

## Turning off

ReachHunt turns a webhook off when:

* your server answers `404` or `410`, or the URL points to an address ReachHunt does not send to
* an event fails all its retries and nothing else reached your server since that event happened

The webhook shows as **Off** in [**Settings → Webhooks**](https://app.reachhunt.ai/settings/webhooks), with the reason. While it is off it gets no events, and events from that time are not sent later.

Fix your server, then turn the webhook back on from its menu in [**Settings → Webhooks**](https://app.reachhunt.ai/settings/webhooks).

## Testing

Open the menu on your webhook in [**Settings → Webhooks**](https://app.reachhunt.ai/settings/webhooks) and select **Send test event**. It sends a sample of the event you pick, with your headers, and shows how your server answered. A test event's `id` starts with `test_`.

## Common mistakes

| Mistake | Do this instead |
| - | - |
| Doing the work before answering | Answer `2xx` first, then do the work or queue it |
| Comparing the secret with `==` | Use a constant-time comparison, such as `crypto.timingSafeEqual` or `hmac.compare_digest` |
| Handling the same event twice | Store each event `id` and skip the ones you've seen |
| Expecting events in order | Compare `createdAt` |
| Pointing the webhook at a URL that redirects | Use the final URL. A `3xx` counts as a failure |
| Answering `404` while you deploy | Answer `503` instead. ReachHunt retries `5xx`, but turns the webhook off on `404` or `410` |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.