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

# Outbound Webhooks

> Send outreach events to your own systems as signed HTTPS requests, and replay any delivery.

Webhooks send events — an invite accepted, a reply arriving, a campaign stalling — to a URL you choose, so you can update a CRM, alert a channel, or start your own automations.

Manage them under **Settings → API & webhooks → Webhooks**, or through the [public API](/integrations-api/public-api).

<Note>Outbound webhooks are included on the **Agency Plus** plan.</Note>

## Add a webhook

1. Enter an **Endpoint URL**. It must be `https://`.
2. Leave **All events (\*)** on, or turn it off and choose specific events.
3. Click **Create webhook**, then reveal and copy its **Secret**. It is shown once.

Each webhook can be switched off and back on without deleting it.

## Events

| Group                  | Events                                                                                                                                            |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Leads**              | `lead.created`, `lead.updated`, `lead.unsubscribed`                                                                                               |
| **Invites**            | `invite.sent`, `invite.accepted`, `invite.withdrawn`                                                                                              |
| **Messages**           | `message.sent`, `message.received`, `message.classified`                                                                                          |
| **Email**              | `email.sent`, `email.opened`, `email.clicked`, `email.bounced`                                                                                    |
| **Enrollments**        | `enrollment.started`, `enrollment.exited`, `enrollment.completed`, `enrollment.held`, `enrollment.resumed`, `enrollment.recovered`                |
| **Tasks and meetings** | `task.created`, `task.completed`, `meeting.booked`                                                                                                |
| **Sequences**          | `sequence.activated`, `sequence.paused`, `sequence.published`, `sequence.throttled`, `sequence.stalled`, `sequence.recovered`, `sequence.webhook` |
| **Senders**            | `sender.connected`, `sender.disconnected`, `sender.reconnected`, `sender.paused`, `sender.level_changed`, `sender.running_dry`                    |
| **Workspace**          | `workspace.billing_recovered`                                                                                                                     |

A few worth knowing:

* **`message.classified`** fires once a reply has been read and given an intent (`interested`, `question`, `not_now`, `not_interested`, `ooo`, `wrong_person`, `unclear`). This is the one to use for "new interested reply".
* **`sequence.stalled`** and **`sender.running_dry`** are the [alerts](/reports/stalled-and-running-dry-alerts). `sequence.recovered` fires when a stall clears.
* **`meeting.booked`** carries `lead_id`, `sender_id`, `provider`, `starts_at` and `booking_id`.
* **`sequence.webhook`** is sent by the [Call webhook](/sequences/creating-managing-sequences/sequence-nodes#integrations) step inside a sequence.

Payloads carry ids, not full records. Fetch the detail with `GET /v1/leads/{id}` or `GET /v1/threads/{id}`.

## Verifying requests

Each delivery is a JSON `POST` with three headers:

```text theme={null}
x-signature    hex HMAC-SHA256 of the raw body, keyed with your secret
x-event        the event name
x-delivery-id  a unique id for this delivery attempt
```

Work out the same HMAC on your side and compare, using a constant-time comparison, before trusting the payload.

<CodeGroup>
  ```js Node.js theme={null}
  import crypto from "node:crypto";

  // rawBody must be the exact bytes we sent, before any JSON parsing
  function isValid(rawBody, signatureHeader, secret) {
    const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
    const a = Buffer.from(expected), b = Buffer.from(signatureHeader || "");
    return a.length === b.length && crypto.timingSafeEqual(a, b);
  }
  ```

  ```python Python theme={null}
  import hmac, hashlib

  def is_valid(raw_body: bytes, signature_header: str, secret: str) -> bool:
      expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
      return hmac.compare_digest(expected, signature_header or "")
  ```
</CodeGroup>

Deliveries can arrive more than once and out of order. Use `x-delivery-id` to drop duplicates.

## Retries and failures

* Answer with a 2xx within **10 seconds**.
* A failed delivery is retried up to **5 times**, with the wait doubling each time (about 2, 4, 8, then 16 minutes).
* A webhook is **switched off automatically after 50 failures in a row**. Turn it back on from the list; this resets the failure count.
* **Recent deliveries** shows the last 100 attempts with status code, attempts and last error. Records are kept for 30 days.

## Replay a delivery

If your endpoint was down, or you fixed a bug and want the event again, click **Replay** on any delivery.

A replay is a **new delivery** carrying the same payload plus `"replayed": true` and `"replay_of": <original id>`. It goes out within about 30 seconds and is signed like any other.

The same thing through the API:

```bash theme={null}
curl "$BASE/webhooks/deliveries?limit=20" -H "Authorization: Bearer $KEY"
curl -X POST "$BASE/webhooks/deliveries/48211/replay" -H "Authorization: Bearer $KEY"
```

## Related articles

* [Webhook Troubleshooting and Common Issues](/integrations-api/webhook-troubleshooting)
* [The Public API](/integrations-api/public-api)
* [Automation Recipes](/integrations-api/automation-recipes)
