# Send leads and hand-offs to your tools with webhooks

> Send new leads, hand-off and live chat requests, form submissions, booked meetings and return requests from intoCHAT to your server, Zapier, Make or n8n as signed JSON: events, payloads, signatures and retries.

A webhook sends an event from your agent to an address you choose, as a signed JSON `POST` request, within seconds. Use it to put new leads into your CRM or to start an automation. It works with your own server and with automation tools that accept incoming webhooks, such as Zapier, Make or n8n: you use the tool's own trigger for incoming webhooks. intoCHAT's own apps for Zapier and Make are built and become available once they are published in those tools' directories; see [Zapier and Make](/en/docs/zapier-and-make). To get these events as messages in a Slack channel, you don't need a webhook: use [Slack alerts](/en/docs/lead-alerts-and-export).

## Events

| Event | Name in the dashboard | Sent when |
| --- | --- | --- |
| `lead.created` | **New lead** | A new contact is saved, from the [lead form](/en/docs/lead-collection) or from contact details the visitor typed in the chat. |
| `handoff.requested` | **Hand-off requested** | The agent emailed a conversation to your team. See [Hand-off by email](/en/docs/handoff). |
| `form.submitted` | **Form submitted** | A visitor sent one of your [chat forms](/en/docs/chat-forms). |
| `booking.created` | **Meeting booked** | A visitor booked a meeting from the chat. See [Booking with Cal.com or Calendly](/en/docs/booking). |
| `live_chat.requested` | **Live chat requested** | The agent asked your team to join the chat, because a visitor asked for a person while someone from your team was online. See [Live chat](/en/docs/live-chat). |
| `return.requested` | **Return requested** | A visitor asked to return or exchange items of an order the agent verified. See [Order status and returns](/en/docs/orders). |

`lead.created` is sent once per contact. A returning visitor who submits the same details again sends no new event, and neither does a chat that adds a phone number to a contact saved earlier in it.

`handoff.requested` is sent only when the email to your team went out. Hand-offs from the **Playground** send it too, marked with `"test": true`. If you connected [helpdesk tickets](/en/docs/helpdesk-tickets), it is sent once the ticket has been created or has failed, usually a few seconds later, and carries the ticket when there is one.

`form.submitted` is sent once per submission. A form can be sent only once per conversation, and a submission never sends `lead.created`, even when the form asks for an email address.

`booking.created` is sent once per booking, after Cal.com or Calendly has accepted it and intoCHAT has saved it. A booking never sends `lead.created`. Bookings from the **Playground** are real bookings and send it too, without a test marker. Changes made later in Cal.com or Calendly, such as a cancellation, send no event.

`live_chat.requested` is sent once per request, when the agent asks your team to join, whether or not anyone joins. A visitor who asks again after the request lapsed or the live chat ended sends a new one. The **Playground** never requests live chat, so it never sends this event.

`return.requested` is sent once per return request, when intoCHAT has saved it. Each conversation can send one request per order, so asking again for the same order sends nothing. Requests from the **Playground** send it too, without a test marker.

## Add a webhook

1. Open your agent and go to the **Leads** tab. **Webhooks** is near the bottom of the tab, above **Slack alerts**.
2. Click **Add webhook**.
3. In **Endpoint URL**, paste the address your server or tool gives you for incoming webhooks.
4. Under **Events to send**, keep the events you want ticked: **New lead**, **Hand-off requested**, **Form submitted**, **Meeting booked**, **Live chat requested** and **Return requested**. All six are ticked for a new webhook. A webhook you added earlier keeps its events, so click **Edit** and tick **Form submitted**, **Meeting booked**, **Live chat requested** or **Return requested** if it should receive those.
5. Leave **Active** on and click **Add webhook**.
6. Copy the signing secret that appears, store it where your receiver can read it, and click **I've saved it**.
7. Click **Send test event** to check the connection. The result appears below the buttons.

The address must start with `https://` and point to a public server. Private and internal network addresses, such as `localhost` or `192.168.1.10`, are refused when you save and checked again before every delivery. Each agent can have up to 5 webhooks.

Webhooks that Zapier, Make or your own code subscribed through the REST API don't count toward the 5; see [Webhooks created by integrations](#webhooks-created-by-integrations). Neither do the webhooks of single forms; see [A webhook for one form](#a-webhook-for-one-form).

To pause a webhook without losing its settings, switch it off; it then shows **Paused**. Events that happen while it is paused are not sent, not even later. A delivery that is waiting for a retry fails at its next try; once the webhook is active again, you can resend it. **Edit** changes the address and the events. **Remove** deletes the webhook and its delivery history.

## Webhooks created by integrations

An integration can subscribe a webhook to your agent through the [REST API](/en/docs/rest-api#subscribe-a-webhook), with one of your API keys. The [Zapier and Make apps](/en/docs/zapier-and-make) do this when you turn on a Zap or add an instant trigger to a scenario.

These webhooks are listed in the **Webhooks** card with the others, with a badge that says where they came from: **via Zapier**, **via Make** or **via API**. They are delivered, signed, retried and logged like the webhooks you add yourself, and you can use **Send test event**, **Recent deliveries**, the **Active** switch and **New secret** on them.

- They have no **Edit** button: the Zap, scenario or code that made them owns the address and the events, and changing them here would break it.
- **Remove** deletes one. The Zap or scenario stays on but gets no more events; turn it off there too, or turn it off and on again to subscribe a new webhook.
- The integration removes its webhook itself when you turn the Zap off or delete the trigger.
- Revoking the API key that created them deletes them. See [Revoke a key](/en/docs/rest-api#revoke-a-key).
- An agent can have up to 25 of them, apart from the 5 you add yourself.

## A webhook for one form

A [chat form](/en/docs/chat-forms#a-webhook-for-one-form) can have one webhook of its own, which receives only that form's `form.submitted` events. You add and manage it in the form's editor on the **Actions** tab, under **Webhook for this form**, not in the **Webhooks** card.

- It has its own signing secret, shown once, and is delivered, signed, retried and logged exactly like the webhooks here, with the same envelope and `form.submitted` data.
- It sends only `form.submitted`, only for its form. Its events can't be changed, and a test event works as described below.
- It doesn't count toward the 5 webhooks of the agent and isn't listed in the **Webhooks** card.
- The agent's webhooks that subscribe to **Form submitted** keep receiving every form's submissions, including those of a form with its own webhook. Each webhook gets its own delivery, with its own ID.
- Deleting the form deletes its webhook and delivery history.

## The signing secret

Each webhook has its own signing secret, starting with `whsec_`. It is shown in full only once, right after you add the webhook or create a new secret. After that, the card shows only its last four characters.

If you lose the secret or think someone else has it, click **New secret**. The old secret stops working at once, so give the new one to your receiver right away.

## What is sent

Every delivery is a `POST` request with a JSON body and these headers:

| Header | Value |
| --- | --- |
| `Content-Type` | `application/json` |
| `User-Agent` | `intoCHAT-Webhooks/1.0` |
| `X-IntoChat-Event` | The event name, for example `lead.created` |
| `X-IntoChat-Delivery` | The delivery ID. It stays the same on every retry. |
| `X-IntoChat-Timestamp` | When this attempt was sent, in Unix seconds |
| `X-IntoChat-Signature` | `sha256=` followed by the signature; see below |

The body always has the same envelope. `id` is the delivery ID, the same value as `X-IntoChat-Delivery`:

```json
{
  "id": "DELIVERY_ID",
  "event": "lead.created",
  "created_at": "2026-10-07T09:30:00.000Z",
  "agent": { "id": "AGENT_ID", "name": "Acme Assistant" },
  "data": { }
}
```

### `lead.created` data

| Field | Content |
| --- | --- |
| `leadId` | The lead's ID |
| `agentId`, `agentName` | The agent that collected the lead |
| `email` | The email address, or `null` |
| `phone` | The phone number, or `null` |
| `name` | The name from the lead form, or `null`. A name is never taken from a chat message. |
| `customFields` | The answers to your custom fields, as a list of `{ "id", "label", "value" }`. Empty when there are none. |
| `source` | `form` or `chat` |
| `conversationId` | The conversation the lead came from, or `null` |
| `collectedAt` | When the lead was saved, as an ISO 8601 time in UTC |
| `identity` | Only for a visitor verified with [identity verification](/en/docs/identity-verification): `{ "userId", "name", "email" }`. Left out otherwise. |

### `handoff.requested` data

| Field | Content |
| --- | --- |
| `conversationId` | The conversation that was handed off |
| `visitorEmail` | The address the visitor gave for the reply |
| `summary` | What the visitor needs, in the agent's words |
| `page` | The page the chat started on, or `null` |
| `country` | The visitor's two-letter country code, or `null` |
| `requestedAt` | When the hand-off was sent, as an ISO 8601 time in UTC |
| `test` | `true` for a hand-off from the Playground, otherwise `false` |
| `ticket` | Only when [helpdesk tickets](/en/docs/helpdesk-tickets) opened a ticket for this hand-off: `{ "provider", "id", "url" }`, where `provider` is `zendesk`, `freshdesk` or `hubspot`, `id` is the ticket's number as a string and `url` opens it in your helpdesk. Left out otherwise. |

### `form.submitted` data

| Field | Content |
| --- | --- |
| `submissionId` | The submission's ID |
| `formId`, `formName` | The form that was sent |
| `agentId`, `agentName` | The agent that showed the form |
| `answers` | The visitor's answers, as a list of `{ "id", "label", "value" }`, in the form's order. Optional fields left empty are not included. |
| `conversationId` | The conversation the form was sent in |
| `submittedAt` | When the submission was saved, as an ISO 8601 time in UTC |
| `identity` | Only for a visitor verified with [identity verification](/en/docs/identity-verification): `{ "userId", "name", "email" }`. Left out otherwise. |

### `booking.created` data

| Field | Content |
| --- | --- |
| `bookingId` | The booking's ID in intoCHAT |
| `provider` | The calendar: `calcom` or `calendly` |
| `providerBookingId` | The booking's ID in Cal.com, or the invitee's URI in Calendly, for example `https://api.calendly.com/scheduled_events/…/invitees/…` |
| `agentId`, `agentName` | The agent the meeting was booked with |
| `eventTypeId`, `eventTypeUri`, `eventTitle` | The event type that was booked: `eventTypeId` is Cal.com's number and `eventTypeUri` is Calendly's URI. The other one is `null`. |
| `startAt`, `endAt` | When the meeting starts and ends, as ISO 8601 times in UTC |
| `timeZone` | The visitor's time zone, for example `Europe/Berlin`, or `UTC` when their browser didn't give one |
| `attendeeName`, `attendeeEmail` | The name and email address the visitor entered |
| `providerStatus` | `accepted`, or `pending` when you must confirm the booking in Cal.com first. Calendly bookings are always `accepted`. |
| `conversationId` | The conversation the meeting was booked in |
| `createdAt` | When the booking was saved, as an ISO 8601 time in UTC |
| `identity` | Only for a visitor verified with [identity verification](/en/docs/identity-verification): `{ "userId", "name", "email" }`. Left out otherwise. |

### `live_chat.requested` data

| Field | Content |
| --- | --- |
| `conversationId` | The conversation the visitor asked for a person in |
| `visitorMessage` | The visitor's latest message, shortened to 500 characters with `…` at the end when longer |
| `page` | The page the chat started on, or `null` |
| `country` | The visitor's two-letter country code, or `null` |
| `requestedAt` | When the agent asked your team to join, as an ISO 8601 time in UTC |

The event doesn't say whether someone joined. Who took the chat over, and when, shows in the [Live chat inbox](/en/docs/live-chat#the-live-chat-inbox) and the transcript.

### `return.requested` data

| Field | Content |
| --- | --- |
| `returnRequestId` | The request's ID in intoCHAT |
| `provider` | The store: `shopify` or `woocommerce` |
| `orderName`, `orderId` | The order as your store shows it, for example `#1042`, and its ID in the store |
| `email` | The order's email address. The visitor proved they know it, or your site's [identity verification](/en/docs/identity-verification) passed it. |
| `kind` | `return` or `exchange` |
| `items` | The items, each `{ "title", "quantity" }`. The title includes the variant when the product was ordered in several, for example `Wool scarf - Blue`. |
| `reason` | Why, in the visitor's words |
| `status` | `requested` |
| `agentId`, `agentName` | The agent the request was made with |
| `conversationId` | The conversation the request was made in |
| `createdAt` | When the request was saved, as an ISO 8601 time in UTC |

Nothing was approved or changed in your store. Handle the return there, then mark it handled in the **Returns** list on the **Leads** tab.

Field names in all events use `camelCase`.

### Test event

**Send test event** sends one request with `"event": "test"` and this data: `{ "message": "This is a test event from intoCHAT. Your webhook is set up correctly." }`. It is sent whatever events the webhook subscribes to, also while it is paused, and it is tried only once. Because its data differs from real events, a tool that maps fields from a sample needs a real event for that, for example a lead you submit yourself on your site.

## Verify the signature

Check every request before you trust it. `X-IntoChat-Signature` is `sha256=` followed by the hex-encoded HMAC-SHA256 of the timestamp, a dot and the raw body, `${timestamp}.${rawBody}`, keyed with the webhook's signing secret. The timestamp is the value of `X-IntoChat-Timestamp`.

- Compute the signature over the body exactly as received, before you parse the JSON.
- Compare in constant time.
- Reject old timestamps, so a captured request can't be replayed. The example below allows 5 minutes.

This Node.js example is the same as the one under **Verify signatures** in the **Webhooks** card, which has a **Copy** button:

```js
const crypto = require("crypto")

// rawBody: the request body exactly as received, before JSON parsing.
function verifyIntoChatWebhook(rawBody, headers, secret) {
  const timestamp = headers["x-intochat-timestamp"]
  const signature = headers["x-intochat-signature"] || ""

  // Reject anything older than 5 minutes, so a captured request can't be replayed.
  const age = Math.abs(Date.now() / 1000 - Number(timestamp))
  if (!timestamp || !(age <= 300)) return false

  const expected = "sha256=" + crypto
    .createHmac("sha256", secret)
    .update(timestamp + "." + rawBody)
    .digest("hex")

  const a = Buffer.from(signature)
  const b = Buffer.from(expected)
  return a.length === b.length && crypto.timingSafeEqual(a, b)
}
```

If the tool you use can't check signatures, treat the webhook address like a password: anyone who knows it can send requests to it.

## Respond and handle duplicates

- Answer with any `2xx` status within 10 seconds. Anything else counts as a failure: another status code, no answer in time, or a connection or certificate error.
- Redirects are not followed. A `3xx` answer counts as a failure, so enter the final address.
- The same event can arrive more than once, for example when your server saved it but answered too late. Use `X-IntoChat-Delivery`, or the body's `id`, to ignore a delivery you have already handled.
- Each webhook gets its own delivery, with its own ID, for the same event.

## Retries

- The first attempt starts right after the event. If it fails, up to two quick retries follow within a few seconds.
- If those fail as well, the delivery shows **Retrying** and a scheduled job tries it again later. Retries stop after 6 attempts in total or 24 hours after the event, whichever comes first. The delivery is then marked **Failed**.
- The scheduled job currently runs once a day. In practice, a delivery that still fails after the quick retries gets one or two more tries, within about a day of the event.
- A delivery to an address that turns out to point to a private network fails at once, without retries.
- Each retry keeps the delivery ID and is signed again with a new timestamp.

## Recent deliveries

Click **Recent deliveries** under a webhook to see its last 20 deliveries. Each shows its status (**Sending**, **Retrying**, **Delivered** or **Failed**), the event, the time, the number of attempts and the HTTP status. A retrying delivery shows when the next try is due, and a delivery that didn't succeed shows the error, for example `No response within 10 seconds.`

A failed delivery has a **Resend** button that tries it once more, right away. Delivery records are deleted after 30 days.

## Next steps

- Choose what the lead form asks: [Lead collection](/en/docs/lead-collection).
- Let visitors reach your team by email: [Hand-off by email](/en/docs/handoff).
- Let your team join the chat: [Live chat](/en/docs/live-chat).
- Collect details with a form under the agent's reply, and give a form a webhook of its own: [Chat forms](/en/docs/chat-forms).
- Let visitors book meetings in the chat: [Booking with Cal.com or Calendly](/en/docs/booking).
- Answer order questions and take return requests: [Order status and returns](/en/docs/orders).
- Post new leads, hand-offs and live chat requests to a Slack channel: [Slack alerts](/en/docs/lead-alerts-and-export#slack-alerts).
- Use intoCHAT in Zapier or Make: [Zapier and Make](/en/docs/zapier-and-make).
- A delivery keeps failing? See [Troubleshooting](/en/docs/troubleshooting).
