When a chat ends, MyLiveChat can post the whole thing — transcript included — to a URL you own. It is the simplest way to get conversations into HubSpot, Salesforce, Pipedrive or a Zapier hook without anyone writing an integration. It is also the piece of our platform most often misunderstood, because a different feature with a very similar name behaves in the opposite way. That distinction is where this guide starts.
Two webhooks, and only one of them retries
There are two separate webhook mechanisms, and confusing them will cost you data.
Event subscriptions are the durable ones. You subscribe an endpoint to event types, every delivery gets a row you can inspect, and a failed delivery is retried on a backoff schedule over several hours before it is finally marked failed. If your server was down for ten minutes, you get the event when it comes back. That is the system our guide on webhooks that do not arrive describes, with its delivery log and its retry buttons.
The chat-end CRM webhook — the one this guide is about — is not that. It is a single URL on your integrations settings, and when a chat ends it is posted once. One attempt. No delivery row, no retry, no log you can browse, no button to re-send. It runs on a background thread with a ten-second timeout so that a slow CRM never delays a chat closing, and if that one attempt fails the payload is gone.
Everything else in this guide follows from that sentence. If your endpoint was unreachable for the second in which a chat ended, that conversation was never delivered and nothing will tell you. Failures are recorded in our internal exception log, which you cannot see.
So the practical rule: if losing an occasional conversation is acceptable — you are enriching a CRM record that a human will look at anyway — this webhook is a fine, cheap choice. If it must not be lost, do not build on a single-attempt delivery. Either use event subscriptions, which retry, or reconcile periodically against the API, treating the webhook as a fast path and the API as the source of truth.
Turning it on
Two fields on the integrations screen: the URL, and an optional shared secret used to sign the payload.
The URL must be HTTPS. Plain HTTP is refused outright rather than warned about, because the payload contains the transcript and a visitor's email — personal data that has no business travelling unencrypted. A loopback address is permitted so you can develop against your own machine; nothing else gets an exemption.
The secret has one behaviour worth knowing before it confuses you: it is masked when the page loads, and saving the masked value keeps the existing secret. That is deliberate, so you can change the URL without retyping a secret you do not have to hand, and so the page never shows it back to you. It does mean two things. You cannot read your secret back out of the interface — store it where you generated it. And to rotate it you must type a genuinely new value; clearing the field or leaving the mask keeps the old one.
Empty URL means the feature is off. There is no separate enable toggle.
What arrives
A single JSON POST, with a version number so the shape can evolve without breaking you, and an event name of chat.ended carried both in the body and in an X-MLC-Event header. Inside, roughly in order:
- Identity and timing — your site id, the session id, when the chat ended, and how long it lasted.
- The agents who took part, with display names.
- The visitor — name and email if they gave them, country and city, browser, the page they were on, and the full custom-data string you passed in.
- Markers — UTM parameters, browser language, timezone, and the visitor's page events. This is the block to read if you care about attribution.
- Service metrics — first response time, total duration, and message counts split by who sent them.
- The transcript — every message, in order, each tagged as being from the visitor, an agent, or the AI.
That last tag is more useful than it looks. Because AI turns are labelled distinctly from agent turns, a receiver can tell how much of the conversation a person actually handled, which is a genuinely hard number to get any other way.
Two blocks are conditional. Geographic and firmographic enrichment appears only when IP lookup has been configured for your account, so do not write a receiver that requires it. And any field the visitor never supplied arrives empty rather than absent — check for empty strings, not for missing keys.
Every finished chat fires, including short ones and ones the visitor abandoned. If you only want qualified conversations, filter on your side using the message counts.
Verifying the signature
When you set a secret, each request carries an X-MLC-Signature header holding a SHA-256 HMAC of the body, hex-encoded and prefixed with the algorithm name.
You should verify it, and there is one way to get this wrong that catches nearly everybody: compute the HMAC over the exact raw bytes you received, before any JSON parsing. If your framework parses the body and you re-serialise it to check the signature, key order or whitespace will differ and every verification will fail while the payload is perfectly valid. Capture the raw body first; most frameworks need to be asked explicitly.
Compare digests with a constant-time comparison rather than string equality, and reject anything that does not match rather than processing it anyway. Without a secret there is no signature at all, and your endpoint is accepting unauthenticated posts from anyone who learns the URL — which is reason enough to set one, since the URL travels through logs and browser history. Our note on securing integrations and keys covers where to keep it.
The settings screen has a button that posts a sample payload to your URL and shows you the status code and the first part of the response. It is genuinely useful for what it is: proof that the URL resolves, that TLS works, that your endpoint answers, and that your signature check passes.
It is not a rehearsal for the real thing, and this is the single most valuable fact in this guide. The test payload differs from a real one in ways that will break a receiver written against it:
- The event name is different — the test announces itself as a test, in both the body and the header. A receiver that switches on the event name and only knows the test value will silently ignore every real chat.
- Several blocks are absent — no agents, no markers, no duration, no enrichment. Fields you never saw during testing will appear in production.
- The transcript is one synthetic line, so nothing about the real message volume, ordering or role mix is exercised.
Use it to prove connectivity, then test your parsing against a real chat you conduct yourself. Accept both event names if you switch on them at all, and prefer not switching on them. A green test result means your endpoint is reachable; it does not mean your receiver works.
The cap that drops rather than truncates
There is a size limit on the payload, and its behaviour is the sharp edge. When a payload exceeds the cap it is not sent at all. Not truncated, not sent without the transcript — dropped, silently, with no notification to you.
The cap is high enough that an ordinary conversation is nowhere near it. What gets close is the unusual chat: a two-hour session, a visitor who pasted logs, a conversation with hundreds of turns. Which means the chats you lose to this are biased towards the long and complicated ones — precisely the conversations you would most want in your CRM.
You cannot raise the cap, so the mitigation is to notice. If your CRM has records for every chat except the marathon ones, this is why, and it is another argument for reconciling against the API if completeness matters to you.
What to measure
Received against conducted. Count chats arriving at your endpoint over a week and compare with the chat count in your reports. A persistent gap is single-attempt losses or the size cap, and the two are distinguishable by whether the missing chats are unusually long.
Your own response times. You have ten seconds. If your handler does real work — a CRM lookup, a record write — return a success status first and do the work afterwards, or a slow dependency of yours will turn into lost conversations.
Signature failures. Any at all, after initial setup, mean either a rotation you did not finish or something posting to your URL that is not us. Both are worth an alert.
Put it into practice
- Assume one attempt. Design for occasional loss, or use a mechanism that retries.
- Always set a secret, and verify it over the raw body before parsing.
- Store the secret where you generated it. The interface will not show it back to you.
- Do not trust the test payload's shape. Prove connectivity with it, then test against a real chat.
- Return quickly, then do the work. Ten seconds is the whole budget.
- Treat empty as normal. Optional fields arrive empty rather than missing.
- Reconcile weekly if completeness matters. The webhook is a fast path, not a ledger.
Used for what it is — a cheap, immediate nudge into another system — the chat-end webhook is excellent, and a receiver worth trusting takes an afternoon. The two things to hold on to are that it will not try twice, and that the test button is not the shape you will actually receive.