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

# Webhooks

> Get an HTTPS request the moment something happens in LeadVector, instead of asking the API over and over.

A webhook endpoint is a URL of yours that LeadVector calls when an event happens: a lead replies, a campaign completes, an email goes out. Each call carries one event, signed so you can check it came from LeadVector.

Webhooks come with every paid plan. Endpoints belong to the team: each one receives the events of every member's work. The team's owner, and members allowed to manage the team, can create and change them.

## Create an endpoint

Give the URL and the event types it should receive (`["*"]` for every type):

```bash theme={null}
curl https://prod.leadvector.app/v1/webhooks \
  -H "x-api-key: YOUR_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "url": "https://hooks.example.com/leadvector",
    "eventTypes": ["lead.replied", "campaign.completed"]
  }'
```

The answer holds the endpoint's signing secret (`whsec_…`). It is shown only this once: store it, and use it to [verify each delivery](/api-reference/webhook-signatures.md).

To receive only some campaigns' events, add `"filters": { "campaignIds": ["…"] }`. The campaign, lead and email events of other campaigns are then left out; events of other kinds are not affected.

Then send it a test event: `POST /v1/webhooks/{id}/test` delivers `webhook.test` to that endpoint alone, and answers with what your receiver said.

## What a delivery looks like

Each delivery is a `POST` with a JSON body, and these headers:

```http theme={null}
POST /leadvector HTTP/1.1
content-type: application/json
user-agent: LeadVector-Webhooks/1
webhook-id: evt_0192b5c4e7a87c3e9d1f2a6b4c8e0d13
webhook-timestamp: 1793785272
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
```

```json theme={null}
{
  "id": "evt_0192b5c4e7a87c3e9d1f2a6b4c8e0d13",
  "type": "lead.replied",
  "timestamp": "2026-11-03T09:41:12.000Z",
  "apiVersion": "2026-11",
  "team": { "id": "0192b5c3-0a1b-7c2d-8e3f-4a5b6c7d8e9f" },
  "actor": { "id": "0192b5c3-1b2c-7d3e-9f4a-5b6c7d8e9f0a", "email": "sam@outreach.example.com" },
  "data": {
    "campaign": { "id": "0192b5c4-1f2e-7a3b-8c4d-5e6f7a8b9c0d", "name": "Q4 outreach" },
    "lead": {
      "contactId": "0192b5c5-2a3b-7c4d-9e5f-6a7b8c9d0e1f",
      "email": "jane.doe@example.com",
      "firstName": "Jane",
      "lastName": "Doe",
      "companyName": "Example Inc."
    },
    "reply": {
      "subject": "Re: A question about Example Inc.",
      "text": "Thanks for reaching out. Could we talk on Thursday?",
      "truncated": false,
      "receivedAt": "2026-11-03T09:41:12.000Z"
    },
    "step": { "number": 2, "sequence": "main" },
    "mailbox": { "id": "0192b5c6-3b4c-7d5e-8f6a-7b8c9d0e1f2a", "email": "sam@outreach.example.com" }
  }
}
```

* **`id`** is the event's id, the same as the `webhook-id` header and the same on every attempt.
* **`type`** says what happened; `data` is that type's payload, described on each event's page in the API reference.
* **`timestamp`** is when it happened, not when it was delivered: a retry carries the same one.
* **`team`** is your team. **`actor`** is the member whose action or campaign caused the event, or `null` when nobody did.
* Ids are the same public ids the API uses: a campaign's `id` is the `uuid` of `/v1/campaigns/{uuid}`.

## Where deliveries come from

If your firewall lets callers in by address, allow LeadVector's:

Deliveries come from this address, over IPv4:

```text theme={null}
162.55.150.98
```

* **Allowing it is optional.** The signature is what proves a delivery came from LeadVector: [verify it](/api-reference/webhook-signatures.md) whether or not your firewall checks the address.
* **A change is announced at least 30 days ahead.** The new address is listed here beside the old one until the move is done, and the owner of every team with an endpoint is emailed.
* **Your endpoint needs an IPv4 address.** A URL whose host has only IPv6 addresses is refused, with the reason `NO_IPV4_ADDRESS`.

## Store it, answer, then do the work

[Check the signature](/api-reference/webhook-signatures.md), store the event somewhere durable (a queue, a database table), then answer with any `2xx`, and do the work from what you stored:

* **A `2xx` means you have it.** The delivery counts as succeeded, and LeadVector won't send it again. If you can't store it, answer an error (`503`, say), and it comes again on the retry schedule.
* **Answer quickly.** An answer that takes too long counts as a failure, and the event comes again.
* **An event you lost after answering** can still be fetched: `GET /v1/events` lists your team's events of the last 30 days, in the shape a delivery has, and `GET /v1/events/{id}` gets one.

The same event can arrive more than once, and events can arrive out of order (a retry of an older one after a newer one). Store each `webhook-id` you have handled and skip one you have seen, and use `timestamp` when order matters.

## Retries and failures

A delivery that fails is tried again, on this schedule:

| Attempt | Sent | Since the event |
| - | - | - |
| 1 | At once | — |
| 2 | 5 seconds after attempt 1 | about 5 seconds |
| 3 | 5 minutes after attempt 2 | about 5 minutes |
| 4 | 30 minutes after attempt 3 | about 35 minutes |
| 5 | 2 hours after attempt 4 | about 3 hours |
| 6 | 5 hours after attempt 5 | about 8 hours |
| 7 | 10 hours after attempt 6 | about 18 hours |
| 8 | 14 hours after attempt 7 | about 32 hours |
| 9 | 20 hours after attempt 8 | about 52 hours |
| 10 | 1 day after attempt 9 | about 76 hours |

* **Success:** any `2xx` answer within 15 seconds (5 seconds to connect). Redirects are not followed: a `3xx` is retried.
* **Gone:** a `410` answer disables the endpoint at once, and what waited for it is cancelled.
* **Waits** vary by up to 20% either way, so retries spread out. A `Retry-After` header asking for longer is honored, up to 1 day.
* **After 10 failed attempts** a delivery fails for good. You can retry it, or replay every failed delivery since a time, for 30 days.
* **At most 5 deliveries** are in flight to one endpoint at once.
* **A failing endpoint:** once every delivery to it has failed for 1 day, the team's owner is emailed; after 5 days, the endpoint is disabled. Enable it again to receive what waited.
* **Events are kept 30 days**, and can be listed (`GET /v1/events`) and delivered again in that time.
* **A team has up to 50 endpoints.** A rotated secret can keep signing beside the new one for up to 1 day.
* **Payload version:** an endpoint is pinned to the version it was created with, now `2026-11`. Within a version fields are only added, never renamed, removed or retyped.

Every endpoint's deliveries are listed with `GET /v1/webhooks/{id}/deliveries`, with each attempt's status code and the start of the answer. `POST /v1/webhooks/{id}/deliveries/{deliveryId}/retry` sends one again now; `POST /v1/webhooks/{id}/replay` sends again every failed or cancelled delivery since a time.

## Event types

| Event | When | Status |
| - | - | - |
| `campaign.created` | A campaign was created. | Sent |
| `campaign.launched` | A draft campaign started sending. | Sent |
| `campaign.paused` | A campaign was paused: by a user, or by LeadVector when its account was deactivated (`reason`). | Sent |
| `campaign.resumed` | A paused campaign started sending again. | Sent |
| `campaign.stopped` | A campaign was stopped for good. | Sent |
| `campaign.completed` | Every lead of a campaign has finished. | Sent |
| `campaign.deleted` | A campaign was deleted. | Sent |
| `lead.added` | A lead joined a campaign: added by hand, imported, or upserted through the API. | Sent |
| `lead.replied` | A lead's first reply in a campaign arrived. Later replies are inbox.message\_received. | Sent |
| `lead.label_changed` | A lead's label changed in the inbox (Lead, Interested, Not interested, Out of office). | Sent |
| `lead.completed` | A lead finished its sequence without replying. | Sent |
| `lead.failed` | A lead cannot be sent to: an invalid address, or a send that failed for good. | Sent |
| `lead.unsubscribed` | A lead unsubscribed. | Coming soon |
| `email.sent` | One step's email was sent to a lead. | Sent |
| `email.failed` | One step's email could not be sent. | Sent |
| `email.opened` | A lead opened an email. | Coming soon |
| `email.link_clicked` | A lead clicked a link in an email. | Coming soon |
| `email.bounced` | An email bounced. | Coming soon |
| `email.auto_reply_received` | An automatic reply (out of office, say) arrived. | Coming soon |
| `inbox.message_received` | A message arrived in the inbox: every one the inbox lists, but warmup emails and the mail from before the mailbox was added. | Sent |
| `inbox.message_sent` | A reply or forward was sent from the inbox. | Sent |
| `email_account.connected` | A mailbox was connected, and its connection check passed. | Sent |
| `email_account.disconnected` | A mailbox lost its connection and needs reconnecting: a user disconnected it, its provider refused its credentials, or its connection check failed (`reason`). | Sent |
| `email_account.reconnected` | A mailbox was connected again, and its connection check passed. | Sent |
| `email_account.paused` | A mailbox was paused: by a user, or by LeadVector when its account was deactivated (`reason`). | Sent |
| `email_account.resumed` | A user resumed a paused mailbox, or one warmup health had paused. | Sent |
| `email_account.cooldown_started` | Warmup health paused a mailbox automatically, for 24 hours. | Sent |
| `email_account.cooldown_ended` | A mailbox paused by warmup health sends again. | Sent |
| `email_account.health_changed` | A mailbox's warmup health changed (HEALTHY, WARNING, RISKY, BAD). | Sent |
| `email_account.deleted` | A mailbox was deleted. | Sent |
| `lookup.email_found` | An email lookup found an address: in the app, the extension or the API (`source`), a repeat included (`creditsCharged` 0). | Sent |
| `lookup.mobile_found` | A mobile lookup found a number. | Sent |
| `lookup.email_verified` | An email verification finished, with its status. | Sent |
| `lookup.reverse_email_found` | A reverse email lookup found the person. | Sent |
| `lookup.person_enriched` | A person enrichment found the person. | Sent |
| `lookup.company_enriched` | A company enrichment found the company. | Sent |
| `bulk_job.completed` | A bulk job finished: every row of its file was looked up, failed or found invalid. | Sent |
| `bulk_job.failed` | A bulk job failed before it started: its file couldn't be read as mapped. | Sent |
| `list.contact_added` | A contact was saved to a list: added or moved in the app, saved from the Chrome extension, or by a search that made a list of its results. | Sent |
| `credits.low` | The team's credits fell below a tenth of its plan's monthly credits. Sent once until the credits are topped up or renewed. | Sent |
| `credits.depleted` | The team's credits can no longer pay for the cheapest lookup. Sent once until the credits are topped up or renewed. | Sent |
| `webhook.test` | A test event, sent to one endpoint on request. | Sent |

`GET /v1/webhooks/event-types` lists them too, each sent one with its JSON Schema and a sample, and the API reference has a page for each.

## Rotating a secret

`POST /v1/webhooks/{id}/rotate-secret` makes a new secret and answers with it. With `overlapHours`, the old one keeps signing beside it for that long, so you can switch without dropping an event: while both sign, `webhook-signature` holds two signatures, and either one is valid.


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