> For the complete documentation index, see [llms.txt](https://docs.reach-book.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.reach-book.com/developers/webhooks-and-automation/webhooks.md).

# Webhooks

Add a webhook in **Workspace settings → Automation & API** and choose its events. Each event is a signed JSON `POST` to your HTTPS address.

* The address must use HTTPS on port 443, without a query string. Redirects aren't followed.
* Respond with any `2xx` status within 10 seconds.
* Timeouts, `408`, `429` and `5xx` responses are retried after 1 minute, 5 minutes, 30 minutes and 2 hours (5 attempts in all).
* An event can arrive more than once, so use its `id` to skip duplicates.

## Events

| Event                      | When                                                           | `data` fields                                                     |
| -------------------------- | -------------------------------------------------------------- | ----------------------------------------------------------------- |
| `prospect.created`         | A prospect is added in the app, by import or through the API.  | `prospect_id`, `company`, `contact_email`                         |
| `inbox.reply.received`     | A prospect replies to an email you sent.                       | `prospect_id`, `company`, `contact_email`                         |
| `conversation.assigned`    | A conversation is assigned to a teammate or unassigned.        | `prospect_id`, `assignee_id`                                      |
| `prospect.outcome.updated` | A sales outcome, meeting or value is recorded for a prospect.  | `prospect_id`, `outcome`, `meeting_at`, `value_minor`, `currency` |
| `opportunity.updated`      | Sent alongside `prospect.outcome.updated` for CRM-style tools. | `prospect_id`, `outcome`, `meeting_at`, `value_minor`, `currency` |
| `meeting.booked`           | A meeting time is set or the outcome becomes Meeting booked.   | `prospect_id`, `outcome`, `meeting_at`, `value_minor`, `currency` |
| `opportunity.won`          | The outcome changes to Won.                                    | `prospect_id`, `outcome`, `meeting_at`, `value_minor`, `currency` |

## Example body

```json
{
    "id": "01JB7Q3M5K8V2X9N4R6T0Y1ZWC",
    "event": "prospect.created",
    "workspace_id": "01J9T2D6F8H3K5M7N9P1Q3R5S7",
    "occurred_at": "2026-10-02T09:30:00+00:00",
    "data": {
        "prospect_id": "01JB7Q3M2C4E6G8J0L2N4Q6S8U",
        "company": "Acme Dental",
        "contact_email": "dana@acmedental.example"
    }
}
```

Headers: `Reachbook-Event-ID`, `Reachbook-Event`, `Reachbook-Timestamp` and `Reachbook-Signature`.

## Verify every webhook

The signature is `v1=` followed by the hexadecimal HMAC-SHA256 of the timestamp, a dot and the raw body, keyed with the webhook's signing secret (it starts with `whk_`). Compare in constant time and reject timestamps older than five minutes.

{% tabs %}
{% tab title="Node.js" %}

```javascript
import crypto from 'node:crypto';

// Use the raw body, e.g. express.raw({ type: 'application/json' }).
export function verifyReachbook(rawBody, headers, secret) {
    const timestamp = headers['reachbook-timestamp'] ?? '';
    const signature = headers['reachbook-signature'] ?? '';
    const expected =
        'v1=' +
        crypto
            .createHmac('sha256', secret)
            .update(`${timestamp}.${rawBody}`)
            .digest('hex');
    const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300;

    return (
        fresh &&
        signature.length === expected.length &&
        crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
    );
}
```

{% endtab %}

{% tab title="PHP" %}

```php
<?php

$payload = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_REACHBOOK_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_REACHBOOK_SIGNATURE'] ?? '';

$expected = 'v1='.hash_hmac('sha256', $timestamp.'.'.$payload, $secret);
$fresh = abs(time() - (int) $timestamp) < 300;

if (! $fresh || ! hash_equals($expected, $signature)) {
    http_response_code(401);
    exit;
}

$event = json_decode($payload, true);
```

{% endtab %}

{% tab title="Python" %}

```python
import hashlib
import hmac
import time


def verify_reachbook(raw_body: bytes, headers, secret: str) -> bool:
    timestamp = headers.get("Reachbook-Timestamp", "")
    signature = headers.get("Reachbook-Signature", "")
    expected = "v1=" + hmac.new(
        secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256
    ).hexdigest()
    fresh = timestamp.isdigit() and abs(time.time() - int(timestamp)) < 300

    return fresh and hmac.compare_digest(expected, signature)
```

{% endtab %}
{% endtabs %}
