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. To get these events as messages in a Slack channel, you don't need a webhook: use Slack alerts.
Events
| Event | Name in the dashboard | Sent when |
|---|---|---|
lead.created | New lead | A new contact is saved, from the lead form 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. |
form.submitted | Form submitted | A visitor sent one of your chat forms. |
booking.created | Meeting booked | A visitor booked a meeting from the chat. See Booking with Cal.com or Calendly. |
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. |
return.requested | Return requested | A visitor asked to return or exchange items of an order the agent verified. See Order status and returns. |
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, 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
- Open your agent and go to the Leads tab. Webhooks is near the bottom of the tab, above Slack alerts.
- Click Add webhook.
- In Endpoint URL, paste the address your server or tool gives you for incoming webhooks.
- 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.
- Leave Active on and click Add webhook.
- Copy the signing secret that appears, store it where your receiver can read it, and click I've saved it.
- 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.
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, with one of your API keys. The Zapier and Make apps 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.
- An agent can have up to 25 of them, apart from the 5 you add yourself.
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:
{
"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: { "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 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: { "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: { "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 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 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:
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
2xxstatus 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
3xxanswer 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'sid, 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.
- Let visitors reach your team by email: Hand-off by email.
- Let your team join the chat: Live chat.
- Collect details with a form under the agent's reply: Chat forms.
- Let visitors book meetings in the chat: Booking with Cal.com or Calendly.
- Answer order questions and take return requests: Order status and returns.
- Post new leads, hand-offs and live chat requests to a Slack channel: Slack alerts.
- Use intoCHAT in Zapier or Make: Zapier and Make.
- A delivery keeps failing? See Troubleshooting.