Skip to content
Documentation

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

EventName in the dashboardSent when
lead.createdNew leadA new contact is saved, from the lead form or from contact details the visitor typed in the chat.
handoff.requestedHand-off requestedThe agent emailed a conversation to your team. See Hand-off by email.
form.submittedForm submittedA visitor sent one of your chat forms.
booking.createdMeeting bookedA visitor booked a meeting from the chat. See Booking with Cal.com or Calendly.
live_chat.requestedLive chat requestedThe 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.requestedReturn requestedA 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

  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.

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:

HeaderValue
Content-Typeapplication/json
User-AgentintoCHAT-Webhooks/1.0
X-IntoChat-EventThe event name, for example lead.created
X-IntoChat-DeliveryThe delivery ID. It stays the same on every retry.
X-IntoChat-TimestampWhen this attempt was sent, in Unix seconds
X-IntoChat-Signaturesha256= 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

FieldContent
leadIdThe lead's ID
agentId, agentNameThe agent that collected the lead
emailThe email address, or null
phoneThe phone number, or null
nameThe name from the lead form, or null. A name is never taken from a chat message.
customFieldsThe answers to your custom fields, as a list of { "id", "label", "value" }. Empty when there are none.
sourceform or chat
conversationIdThe conversation the lead came from, or null
collectedAtWhen the lead was saved, as an ISO 8601 time in UTC
identityOnly for a visitor verified with identity verification: { "userId", "name", "email" }. Left out otherwise.

handoff.requested data

FieldContent
conversationIdThe conversation that was handed off
visitorEmailThe address the visitor gave for the reply
summaryWhat the visitor needs, in the agent's words
pageThe page the chat started on, or null
countryThe visitor's two-letter country code, or null
requestedAtWhen the hand-off was sent, as an ISO 8601 time in UTC
testtrue for a hand-off from the Playground, otherwise false
ticketOnly 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

FieldContent
submissionIdThe submission's ID
formId, formNameThe form that was sent
agentId, agentNameThe agent that showed the form
answersThe visitor's answers, as a list of { "id", "label", "value" }, in the form's order. Optional fields left empty are not included.
conversationIdThe conversation the form was sent in
submittedAtWhen the submission was saved, as an ISO 8601 time in UTC
identityOnly for a visitor verified with identity verification: { "userId", "name", "email" }. Left out otherwise.

booking.created data

FieldContent
bookingIdThe booking's ID in intoCHAT
providerThe calendar: calcom or calendly
providerBookingIdThe booking's ID in Cal.com, or the invitee's URI in Calendly, for example https://api.calendly.com/scheduled_events/…/invitees/…
agentId, agentNameThe agent the meeting was booked with
eventTypeId, eventTypeUri, eventTitleThe event type that was booked: eventTypeId is Cal.com's number and eventTypeUri is Calendly's URI. The other one is null.
startAt, endAtWhen the meeting starts and ends, as ISO 8601 times in UTC
timeZoneThe visitor's time zone, for example Europe/Berlin, or UTC when their browser didn't give one
attendeeName, attendeeEmailThe name and email address the visitor entered
providerStatusaccepted, or pending when you must confirm the booking in Cal.com first. Calendly bookings are always accepted.
conversationIdThe conversation the meeting was booked in
createdAtWhen the booking was saved, as an ISO 8601 time in UTC
identityOnly for a visitor verified with identity verification: { "userId", "name", "email" }. Left out otherwise.

live_chat.requested data

FieldContent
conversationIdThe conversation the visitor asked for a person in
visitorMessageThe visitor's latest message, shortened to 500 characters with … at the end when longer
pageThe page the chat started on, or null
countryThe visitor's two-letter country code, or null
requestedAtWhen 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

FieldContent
returnRequestIdThe request's ID in intoCHAT
providerThe store: shopify or woocommerce
orderName, orderIdThe order as your store shows it, for example #1042, and its ID in the store
emailThe order's email address. The visitor proved they know it, or your site's identity verification passed it.
kindreturn or exchange
itemsThe items, each { "title", "quantity" }. The title includes the variant when the product was ordered in several, for example Wool scarf - Blue.
reasonWhy, in the visitor's words
statusrequested
agentId, agentNameThe agent the request was made with
conversationIdThe conversation the request was made in
createdAtWhen 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 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

View as Markdown