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

> Creates a webhook that receives real-time POST callbacks for selected events.

# Add Webhook

export const SnippetObjectReference = ({objectName, objectPath = null}) => {
  const lowerCaseObjectName = objectName.toLowerCase();
  if (lowerCaseObjectName === 'lead' || lowerCaseObjectName === 'leads') {
    return <Note>
        This endpoint uses the <a href={`/api-reference/objects-definitions/${objectPath}`}>{objectName} object</a>. Make sure to also check the <a href={`/api-reference/objects-definitions/${lowerCaseObjectName === 'lead' ? 'contact' : 'lead'}`}>{lowerCaseObjectName === 'lead' ? 'Contact' : 'Lead'} object</a> to understand the distinction between the two.
      </Note>;
  }
  return <Note>
      This endpoint uses the <a href={`/api-reference/objects-definitions/${objectPath}`}>{objectName} object</a>.
    </Note>;
};

<SnippetObjectReference objectName="Webhook" objectPath="webhook" />

## Limits and conflicts

A workspace holds at most **200 webhooks**, and a `targetUrl` can only be registered once. Both limits answer `409`, with a JSON body whose `error` field tells them apart:

| `error` | Cause | What to do |
| - | - | - |
| `Too many webhooks (max 200 per team, disabled webhooks included)` | The workspace is at the cap. | Free a slot with [Delete Webhook](/api-reference/endpoints/webhooks/delete-webhook). Disabled webhooks count too — see below. |
| `A webhook with this targetUrl already exists` | Another webhook already points to that URL. | Reuse it, or register a distinct URL. |

<Warning>
  Uniqueness is on `targetUrl` alone, regardless of `type` and `campaignId`. The
  same URL cannot be registered twice to subscribe to two event types — either
  register it once without a `type` (it then receives every event, and you filter
  on your side), or give each subscription its own URL.
</Warning>

### Disabled webhooks still count toward the cap

When a delivery fails in a way that says the destination is gone — your endpoint answers `404` or `410`, or its host does not resolve — lemlist disables the webhook instead of deleting it, and notifies its owner. [Get Many Webhooks](/api-reference/endpoints/webhooks/get-many-webhooks) keeps returning it, carrying `disabled: true`, `disabledReason: "deliveryError"`, `disabledAt` and `lastErrorStatus`.

A disabled webhook stops firing but keeps its slot, and nothing purges it. A workspace showing a handful of live webhooks can therefore be refused a new one. When you check the remaining room, count **every** entry `GET /hooks` returns, not just the active ones, and delete those you no longer need.

## Available event types

When creating a webhook, you can subscribe to specific events using the `type` field. Events are organized by category below.

### Lead state groups

These aggregate multiple activity types into a single lead-state change event.

| Event | Description |
| - | - |
| `contacted` | Lead was contacted (email sent, LinkedIn message, API call, etc.) |
| `hooked` | Lead opened a message (email, LinkedIn, WhatsApp) |
| `attracted` | Lead clicked a link or accepted a LinkedIn invite |
| `warmed` | Lead replied (email, LinkedIn, WhatsApp, SMS) |
| `interested` | Lead marked as interested |
| `notInterested` | Lead marked as not interested |

### Email activities

| Event | Description |
| - | - |
| `emailsSent` | Email sent |
| `emailsOpened` | Email opened |
| `emailsClicked` | Link clicked in email |
| `emailsReplied` | Email replied |
| `emailsBounced` | Email bounced |
| `emailsFailed` | Email failed to send |
| `emailsInterested` | Lead marked as interested via email |
| `emailsNotInterested` | Lead marked as not interested via email |

### Unsubscribe activities

| Event | Description |
| - | - |
| `entityUnsubscribed` | Contact unsubscribed from all communications — e.g. clicked the unsubscribe link or was marked as do-not-contact |
| `variableUnsubscribed` | A specific contact variable (email address, phone number, LinkedIn profile) was unsubscribed |
| `emailsUnsubscribed` | Lead stopped because their email or domain is on the unsubscribe list. Also matches `apiUnsubscribed` and `manualUnsubscribed` activities |

<Note>
  Since March 30, 2026, a contact unsubscribing emits `entityUnsubscribed` (whole
  contact) or `variableUnsubscribed` (single variable) instead of
  `emailsUnsubscribed`. If your integration listens to `emailsUnsubscribed`,
  also subscribe to `entityUnsubscribed` and `variableUnsubscribed` to keep
  receiving unsubscribe events.
</Note>

### LinkedIn activities

| Event | Description |
| - | - |
| `linkedinSent` | LinkedIn message sent |
| `linkedinOpened` | LinkedIn message opened |
| `linkedinReplied` | LinkedIn message replied |
| `linkedinInterested` | Lead marked as interested via LinkedIn |
| `linkedinNotInterested` | Lead marked as not interested via LinkedIn |
| `linkedinSendFailed` | LinkedIn message failed to send |
| `linkedinVisitDone` | LinkedIn profile visit completed |
| `linkedinVisitFailed` | LinkedIn profile visit failed |
| `linkedinFollowDone` | LinkedIn follow completed |
| `linkedinFollowFailed` | LinkedIn follow failed |
| `linkedinFollowSkipped` | LinkedIn follow skipped |
| `linkedinInviteDone` | LinkedIn invite sent |
| `linkedinInviteFailed` | LinkedIn invite failed |
| `linkedinInviteAccepted` | LinkedIn invite accepted |
| `linkedinEndorseDone` | LinkedIn endorsement completed |
| `linkedinEndorseFailed` | LinkedIn endorsement failed |
| `linkedinEndorseSkipped` | LinkedIn endorsement skipped |
| `linkedinVoiceNoteDone` | LinkedIn voice note sent |
| `linkedinVoiceNoteFailed` | LinkedIn voice note failed |
| `linkedinLikeLastPostDone` | LinkedIn like last post completed |
| `linkedinLikeLastPostNoPost` | LinkedIn like last post — no post found |
| `linkedinLikeLastPostFailed` | LinkedIn like last post failed |
| `linkedinWithdrawInvitationDone` | LinkedIn invite withdrawn |
| `linkedinWithdrawInvitationFailed` | LinkedIn invite withdrawal failed |

### WhatsApp activities

| Event | Description |
| - | - |
| `whatsappMessageSent` | WhatsApp message sent |
| `whatsappMessageDelivered` | WhatsApp message delivered |
| `whatsappMessageOpened` | WhatsApp message opened |
| `whatsappReplied` | WhatsApp message replied |
| `whatsappMessageFailed` | WhatsApp message failed |

### SMS activities

| Event | Description |
| - | - |
| `smsSent` | SMS sent |
| `smsDelivered` | SMS delivered |
| `smsReplied` | SMS replied |
| `smsFailed` | SMS failed |

### Call activities

| Event | Description |
| - | - |
| `aircallCreated` | Call created |
| `aircallEnded` | Call ended |
| `aircallDone` | Call task completed |
| `aircallInterested` | Lead marked as interested via call |
| `aircallNotInterested` | Lead marked as not interested via call |
| `callRecordingDone` | Call recording ready |
| `callTranscriptDone` | Call transcript ready |

### API activities

| Event | Description |
| - | - |
| `apiDone` | API step executed |
| `apiInterested` | Lead marked as interested via API |
| `apiNotInterested` | Lead marked as not interested via API |
| `apiFailed` | API step failed |

### Manual activities

| Event | Description |
| - | - |
| `manualInterested` | Lead marked as interested manually |
| `manualNotInterested` | Lead marked as not interested manually |

### Task activities

| Event | Description |
| - | - |
| `annotated` | Lead annotated |

### Workspace activities

| Event | Description |
| - | - |
| `paused` | Lead paused |
| `resumed` | Lead resumed |
| `stopped` | Lead stopped |
| `campaignComplete` | Campaign completed |
| `customDomainErrors` | Custom domain error detected |
| `connectionIssue` | Email account connection issue |
| `sendLimitReached` | Daily send limit reached |
| `lemwarmPaused` | Lemwarm paused |

### Enrichment

| Event | Description |
| - | - |
| `enrichmentDone` | Enrichment completed |
| `enrichmentError` | Enrichment failed |

### Contact sourcing

| Event | Description |
| - | - |
| `contactSourcingDone` | A contact-sourcing run finished; the payload carries the buying committee it found |
| `contactSourcingFailed` | A contact-sourcing run failed; the payload carries the reason |

The payload of both is the same shape [Get Contact Sourcing Run](/api-reference/endpoints/contact-sourcing/get-contact-sourcing-run) answers with, under a `data` key.

<Note>
  Subscribe to `contactSourcingFailed` as well as `contactSourcingDone`: a run
  that fails never emits the done event, so an integration listening only for
  the latter waits for a result that is not coming.
</Note>

### Inbox activities

| Event | Description |
| - | - |
| `inboxLabelUpdated` | Inbox conversation label changed |

### Signal Agents

| Event | Description |
| - | - |
| `signalRegistered` | New signal detected by a Signal Agent |

### Deliverability hub

| Event | Description |
| - | - |
| `deliverabilityAlertTriggered` | Deliverability alert triggered |

### Deprecated events

These events have been removed from the API. Use the replacements below.

| Removed event | Replacement |
| - | - |
| `skipped` | No direct replacement |
| `emailsSendFailed` | Use `emailsFailed` instead |
| `opportunitiesDone` | Use `annotated` or task-level webhooks instead |

## Verifying webhook authenticity

You can pass an optional `secret` in the request body when creating the webhook. It works like a shared password between lemlist and your endpoint:

* Stored encrypted at rest.
* Never returned by `GET /hooks` or any other endpoint.
* Immutable — it cannot be changed after creation. To rotate it, delete the webhook and create a new one.
* Sent back to your endpoint as a `secret` field in the JSON body of every webhook call, so you can verify the request originated from lemlist.

## Webhook payload

Each event is delivered as a `POST` to your `targetUrl` with a JSON body that mirrors the underlying activity record. Common fields:

| Field | Type | Description |
| - | - | - |
| `_id` | string | Unique activity identifier (`act_…`) |
| `type` | string | Event type (e.g. `emailsSent`, `emailsReplied`) |
| `teamId` | string | Team the activity belongs to |
| `createdAt` | ISO date | When the activity happened |
| `campaignId` | string | Source campaign (absent for non-campaign events) |
| `campaignName` | string | Campaign display name |
| `sequenceId` | string | Sequence the task belonged to |
| `sequenceStep` | number | 0-based step index in the sequence |
| `stepId` | string | Stable identifier of the step (survives a reorder — see note below) |
| `leadId` | string | Lead targeted by the activity (campaign events only) |
| `contactId` | string | Contact the activity relates to |
| `leadEmail`, `leadFirstName`, `leadLastName`, `leadCompanyName`, `leadPhone`, `leadPicture` | string | Lead identity as it was when the event happened. Campaign events only, and only the variables the lead had at that moment |
| `email`, `firstName`, `lastName`, `linkedinUrl`, `phone`, `jobTitle`, `companyName`, … | string | Current contact record (its fields, custom fields and company), added at delivery time to every event that has a `contactId`. Never overrides a key already set on the event |
| `sendUserId`, `sendUserEmail`, `sendUserName` | string | Sender (lemlist user) identity |
| `subject` | string | Email subject (email events only) |
| `secret` | string | Echoed back if a `secret` was set on the webhook |

<Note>
  **Non-campaign events carry no `lead*` keys.** An activity created from a
  contact page or the inbox rather than from a campaign step (a LinkedIn message
  sent outside a campaign, its opens and replies) has a
  `contactId` but no `leadId`, so `leadFirstName`, `leadLastName` and `leadEmail`
  are absent: there is no lead to take them from. `firstName`, `lastName` and
  `email` are still present, read from the contact. To get a name on every
  event, read `firstName` / `lastName`, or `leadFirstName` / `leadLastName` with
  a fallback to them.
</Note>

<Note>
  `stepId` identifies the step itself, while `sequenceStep` is a position that
  shifts when a sequence's steps are reordered — prefer `stepId` when you store a
  reference. Both are sent: `sequenceStep` is unchanged. `stepId` is present on
  every activity created from now on, and older activities gain it progressively
  as historical records are backfilled.
</Note>

### Email recipients (`to` / `cc` / `bcc`)

Email events (`emailsSent`, `emailsReplied`, `emailsBounced`) carry the full recipient lists as arrays of `{address, name}`. Addresses are lowercased. Fields are omitted when empty.

```json theme={"theme":"dracula"}
{
  "type": "emailsSent",
  "to":  [{ "address": "lead@example.com", "name": "Alice Doe" }],
  "cc":  [{ "address": "manager@example.com", "name": "" }],
  "bcc": [{ "address": "archive@crm.io", "name": "" }]
}
```

* `to` — primary recipient(s). For outbound sends, this is the lead. For inbound replies, this is the mailbox owner; multi-recipient `to` lines (e.g. reply-all where the lead manually added people) are preserved.
* `cc` — explicit CC recipients on the email.
* `bcc` — only present for outbound `emailsSent` events when the sender has a hidden BCC configured in **Settings → Integrations** (`users.lemlist.bcc`). Inbound BCC is never visible because SMTP strips it for non-BCC'd recipients.

### Third-party reply flag

When a reply on a thread comes from an address that does **not** match the original lead's known contact emails, lemlist attributes the reply to the third-party sender rather than to the lead, and the event carries an extra flag:

```json theme={"theme":"dracula"}
{
  "type": "emailsReplied",
  "isThirdPartyReply": true
}
```

Third-party reply payloads omit `campaignId`, `leadId`, and `sequenceId` (since the reply is no longer attached to the original campaign flow). Use the flag to route these events differently if needed.

## Examples

<CodeGroup>
  ```json all events theme={"theme":"dracula"}
  {
  	"targetUrl": "https://webhook.site/your-id"
  }
  ```

  ```json specific event and campaign theme={"theme":"dracula"}
  {
  	"targetUrl": "https://webhook.site/your-id",
  	"type": "linkedinInterested",
  	"campaignId": "cam_A1B2C3D4E5F6G7H8I9",
  	"isFirst": true
  }
  ```

  ```json whatsapp events theme={"theme":"dracula"}
  {
  	"targetUrl": "https://webhook.site/your-id",
  	"type": "whatsappReplied"
  }
  ```

  ````json signal agent signals theme={"theme":"dracula"}
  {
  	"targetUrl": "https://webhook.site/your-id",
  	"type": "signalRegistered"
  ```json with shared secret
  {
  	"targetUrl": "https://webhook.site/your-id",
  	"secret": "s3cret-shared-with-my-endpoint"
  }
  ````
</CodeGroup>

<Note>
  Tip: You can include an optional `zapId` if you use Zapier to track the webhook mapping on your side.
</Note>


## OpenAPI

````yaml post /hooks
openapi: 3.0.0
info:
  title: lemlist API
  version: 1.0.0
  description: >-
    Welcome to the lemlist Developer Documentation.


    lemlist is very customizable and open. You'll find on this page all the API
    and integration you can do with lemlist.


    # Rate Limit


    lemlist's API rate limits requests in order to prevent abuse and overload of
    our services.  

    Rate limits are applied on all routes and per API key performing the
    request.  

    The rate limits are **20** requests per **2** seconds.  

    The response provides any information you may need about it:


    | Header | Description |

    | --- | --- |

    | Retry-After | The number of seconds in which you can retry |

    | X-RateLimit-Limit | The maximum requests in that time |

    | X-RateLimit-Remaining | The number of remaining requests you can make |

    | X-RateLimit-Reset | The date when the rate limit will reset |


    _Example of values for the rate limit headers_


    ``` json

    {
        "Retry-After": 2,
        "X-RateLimit-Limit": 20,
        "X-RateLimit-Remaining": 7,
        "X-RateLimit-Reset" : "Tue Feb 16 2021 09:02:42 GMT+0100 (Central European Standard Time)"
    }

     ```

    # Definitions


    ## Team


    A team is the entity of lemlist that can handle users and billing.


    ## Credits


    Credits are the coins a team uses to enrich emails, LinkedIn URLs, etc. via
    the enrich route. Each enrichment feature needs a certain amount of credits
    to run.


    ## User


    You use a user account to connect to lemlist and send messages via the
    connected emails or LinkedIn account.


    ## Campaign


    A campaign is the entity to automate outreach. A campaign has multiple
    sequences composed of steps.


    ## Lead


    A lead is a person that you try to contact via a campaign.


    ## Activity


    An activity is the history of all the steps.


    ## Unsubscribe


    An unsubscribe occurs when a person decides they don't want to receive
    emails from you anymore.


    # Authentication


    All API routes use the dedicated subdomain `api.lemlist.com`.


    lemlist uses API keys to allow access to the API. You can get your lemlist
    API key at our [integration
    page](https://app.lemlist.com/settings/integrations).


    You need to add the `Authorization` header using the `Basic` authentication
    type. `login:password` **where the login is always empty and the password is
    the API key**.


    ⚠️ **Don't forget to add the semicolon (**`:`**) before your API key in curl
    command.**


    > To authorize, use this code: 
      

    ``` shell

    curl https://api.lemlist.com/api/team \
      --user ":YourApiKey"

     ```

    **Make sure to replace** **`YourApiKey`** **with your API key.**


    # Give feedback


    If you want to report a bug, ask for data, or share with us a use case,
    please fill this [form](https://lemlist.typeform.com/to/mfVlkyGf). It will
    help us centralize your needs!
servers:
  - url: https://api.lemlist.com/api
security:
  - basicAuth: []
tags:
  - name: Enrichment Providers
    description: >-
      The data providers lemlist can call to find an email or a phone.


      Internal providers are paid with lemlist credits and always available.
      External providers run on your own account: connect one by saving its API
      key, and it becomes available to your waterfalls. `GET
      /enrichments/providers` is the catalog of provider ids accepted by the
      waterfall endpoints.
  - name: Enrichment Waterfalls
    description: >-
      Control which data providers lemlist calls when enriching a contact, and
      in which order.


      A waterfall is an ordered list of providers for one enrichment `type`
      (`email` or `phone`). Providers are tried one after the other until one
      returns a result.


      Every team has exactly one default waterfall per type (`isDefault: true`).
      While its `editor` is `lemlist` it follows the lemlist behaviour:
      providers you connected with your own API key are tried first, then
      internal providers in a random order. Once a team member edits its
      provider list, `editor` becomes that user's id and the list is followed as
      is; resetting it hands it back to `lemlist`.


      Custom waterfalls (`isDefault: false`) carry `conditions` on the contact
      being enriched, keyed by contact property; every key present must match.
      When a contact is enriched, lemlist runs a single waterfall: the custom
      waterfall of the requested type whose conditions match the contact, or the
      default waterfall when none does. If several match, the one with the most
      condition keys wins, then the most recently updated. Only that waterfall
      runs; when it finds nothing, the enrichment ends without falling back to
      another one.
paths:
  /hooks:
    post:
      tags:
        - Webhooks
      summary: Add Webhook
      parameters:
        - name: campaignId
          in: query
          required: false
          description: Webhook for specific campaign
          example: cam_123
          schema:
            type: string
        - name: isFirst
          in: query
          required: false
          description: Webhook for first activity only
          example: 'true'
          schema:
            type: boolean
        - name: zapId
          in: query
          required: false
          description: Zapier ID
          example: '123456789'
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                targetUrl:
                  type: string
                  format: uri
                  description: The URL that will receive webhook POST requests.
                type:
                  type: string
                  description: >-
                    Optional event type to subscribe to. When omitted, all
                    events are sent. See the full categorized list in the
                    endpoint documentation.
                  enum:
                    - contacted
                    - hooked
                    - attracted
                    - warmed
                    - interested
                    - notInterested
                    - emailsSent
                    - emailsOpened
                    - emailsClicked
                    - emailsReplied
                    - emailsBounced
                    - emailsFailed
                    - emailsInterested
                    - emailsNotInterested
                    - emailsUnsubscribed
                    - entityUnsubscribed
                    - variableUnsubscribed
                    - linkedinSent
                    - linkedinOpened
                    - linkedinReplied
                    - linkedinInterested
                    - linkedinNotInterested
                    - linkedinSendFailed
                    - linkedinVisitDone
                    - linkedinVisitFailed
                    - linkedinFollowDone
                    - linkedinFollowFailed
                    - linkedinFollowSkipped
                    - linkedinInviteDone
                    - linkedinInviteFailed
                    - linkedinInviteAccepted
                    - linkedinEndorseDone
                    - linkedinEndorseFailed
                    - linkedinEndorseSkipped
                    - linkedinVoiceNoteDone
                    - linkedinVoiceNoteFailed
                    - linkedinLikeLastPostDone
                    - linkedinLikeLastPostNoPost
                    - linkedinLikeLastPostFailed
                    - linkedinWithdrawInvitationDone
                    - linkedinWithdrawInvitationFailed
                    - whatsappMessageSent
                    - whatsappMessageDelivered
                    - whatsappMessageOpened
                    - whatsappReplied
                    - whatsappMessageFailed
                    - smsSent
                    - smsDelivered
                    - smsReplied
                    - smsFailed
                    - aircallCreated
                    - aircallEnded
                    - aircallDone
                    - aircallInterested
                    - aircallNotInterested
                    - apiDone
                    - apiInterested
                    - apiNotInterested
                    - apiFailed
                    - manualInterested
                    - manualNotInterested
                    - paused
                    - resumed
                    - stopped
                    - campaignComplete
                    - customDomainErrors
                    - connectionIssue
                    - sendLimitReached
                    - lemwarmPaused
                    - annotated
                    - enrichmentDone
                    - enrichmentError
                    - callRecordingDone
                    - callTranscriptDone
                    - inboxLabelUpdated
                    - signalRegistered
                    - deliverabilityAlertTriggered
                    - contactSourcingDone
                    - contactSourcingFailed
                secret:
                  type: string
                  description: >-
                    Optional shared secret. Stored encrypted, never returned by
                    GET, and immutable once set. Sent back to your endpoint as a
                    `secret` field in the body of every webhook call so you can
                    verify the request originated from lemlist.
              required:
                - targetUrl
            example:
              targetUrl: https://webhook.site/00000000-0000-0000-0000-000000000000
              type: linkedinInterested
              secret: s3cret-shared-with-my-endpoint
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  _id:
                    type: string
                    description: Unique identifier for the webhook
                  targetUrl:
                    type: string
                    description: The URL where the webhook will send data
                  createdAt:
                    type: string
                    format: date-time
                    description: Timestamp when the webhook was created
                  type:
                    type: string
                    description: Type of webhook event
              example:
                _id: hoo_GvYkkWIcdPf2WsBDY
                targetUrl: https://n8n.example.com/webhook/lemlist-webhook-surprise
                createdAt: '2025-10-29T00:47:42.035Z'
                type: linkedinInterested
        '400':
          description: Bad team
          content:
            text/plain:
              example: Bad team
        '401':
          description: Unauthorized
        '404':
          description: Webhook not found
          content:
            text/plain:
              example: Webhook not found
        '409':
          description: >-
            The webhook was not created. Two distinct cases, told apart by the
            `error` message: the workspace is at its webhook cap, or the
            `targetUrl` is already registered.
          content:
            application/json:
              examples:
                tooManyWebhooks:
                  summary: >-
                    The workspace reached the 200-webhook cap — delete unused
                    webhooks, including disabled ones, before retrying
                  value:
                    error: >-
                      Too many webhooks (max 200 per team, disabled webhooks
                      included)
                duplicateTargetUrl:
                  summary: >-
                    A webhook already points to this targetUrl — uniqueness is
                    on the URL alone, regardless of `type` or `campaignId`
                  value:
                    error: A webhook with this targetUrl already exists
components:
  securitySchemes:
    basicAuth:
      type: http
      scheme: basic

````

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