6 min Read

Vapi Has No End-of-Chat Webhook. Here Is What To Do Instead.

Vapi has no end-of-chat webhook, so chat transcripts never arrive on their own. The capture pattern that holds up in production, and the two traps that will cost you an afternoon.

Nikhil Sharma
Vapi Has No End-of-Chat Webhook. Here Is What To Do Instead. - Digital Solutions Ninja blog

Key takeaways

  • Vapi has no end-of-chat webhook because a chat session has no hangup, so there is no moment the platform can point at and call the end
  • Capture what you cannot afford to lose during the conversation, using a tool the assistant calls itself, rather than waiting for an event that never arrives
  • Reconcile the full transcript afterwards on a delayed job, and let that half be allowed to fail
  • Liquid variables like assistant.id render as pills in the dashboard but do not resolve in chat-mode tool fields, which is why tool configuration ends up per-assistant

Vapi has no end-of-chat webhook. Voice calls get end-of-call-report, which arrives once the call is over with the final transcript attached. Chat has no equivalent event. Nothing tells you a conversation finished, and nothing hands you what was said.

This is not an oversight waiting to be patched. A phone call has a hangup, which is an unambiguous moment the platform can detect and report. A chat session has no such moment. Someone reads the answer they wanted and closes the tab, or leaves it open in a background tab for two days, or comes back an hour later and asks one more thing. There is no event because there is no ending.

Once you accept that, the design problem changes shape. You stop asking how to be notified and start asking what you cannot afford to lose.

Why the obvious workarounds do not hold

Three approaches get suggested every time this comes up, and each one fails in a way worth understanding before you build on it.

  • Wait for chat.deleted. It fires, but it carries no transcript and no summary. It also describes a data lifecycle event rather than a conversation ending, and almost nobody deletes a chat they have finished with.
  • Send every message to your server. This works, in the sense that the data arrives. You now handle a webhook per message and still have no way to know which message was the last one, which is the thing you were trying to find out.
  • Detect the end in the browser. Unload and visibility events are best effort by design, they are least reliable on mobile, and the visitor who closes the tab without warning is disproportionately the one you needed to capture.

All three share the same flaw. They are trying to recover the conversation after the person has gone, which is the one moment when you have no leverage left.

Two traps that will cost you an afternoon

Before the pattern, two things that are genuinely hard to discover, because in both cases the tooling tells you everything is fine.

Liquid variables do not resolve in chat-mode tool fields. Values like assistant.id and chat.id are how you would normally avoid hardcoding identity into a tool. In the dashboard they render as neat pills, exactly as they do for voice, so the configuration looks correct. In chat mode they never resolve, and your endpoint receives them unsubstituted. The workable answer is to stop fighting it: keep tools per assistant with the identifier written in directly, and generate that configuration programmatically later if you are onboarding clients at volume.

The list endpoint omits message content. Listing recent chats gives you ids, timestamps and metadata, and no record of what anyone said. The messages only appear when you fetch a chat individually. So a reconciliation job is not one call, it is one call to find candidates and then one call per candidate to hydrate it. This matters for how you schedule it, and it is not obvious until you have already written the loop that does not work.

The pattern that holds up

Split the problem in two, by how much each half matters.

Capture during the conversation, not after it. Give the assistant a save tool and instruct it to call the tool as soon as it has the things you actually need, typically a name, a way to reach the person, and why they came. The model calls it mid-conversation, while the visitor is still there. If they close the tab immediately afterwards, you already have the lead. This half is not allowed to fail, so it runs on the only mechanism under your control: the assistant's own behaviour during the session.

Reconcile afterwards, on a delay. A scheduled job runs roughly ten minutes behind, lists recent chats, hydrates each one, and attaches the full transcript to the record the tool already created. Ten minutes is not a magic number. It is long enough that a conversation which looked finished usually is, and short enough that someone checking the dashboard over a coffee sees the whole thing. This half is allowed to fail, because failing means a lead with a slightly thinner record rather than no lead at all.

One consequence of the first trap: because the tool call and the chat record do not share an identifier you can rely on, the two halves get matched on content instead, using the contact details that appear in both. It is less elegant than joining on an id and it is what actually works.

A note on which model runs the chat

Worth knowing if you are running voice and chat from one configuration. A small, fast model is the right default on the phone, where every extra hundred milliseconds is audible and telephony has already spent most of your latency budget getting the audio there.

That same model is the wrong choice for chat. Chat has no latency pressure, nobody notices an extra second of typing indicator, and the small model skipped tool calls often enough to matter. Since the entire capture strategy depends on the assistant reliably calling one tool, that is not a trade worth making. Separate assistants, separate models, separate prompts per channel.

The general shape of this

Vapi is a specific instance of a problem that turns up whenever you integrate with a platform you do not control. The event you need does not exist, and the events that do exist sit adjacent to it.

The instinct is to find a clever proxy for the missing event. The better move is to sort your requirements by what you cannot afford to lose, capture that during the window when you still have influence, and let everything else arrive late and imperfectly on a job that is permitted to fail.

Applied here, that means the lead is captured by the assistant mid-conversation and the transcript catches up later. The version that waits for an end-of-chat webhook captures nothing at all.

If you are building on a platform whose docs stop exactly where your problem starts, that is most of what custom app development actually is. The scoping engagement that works out what you are really up against, before anyone commits to a number, is the MVP Roadmap.

FAQ

Quick answers to the most common questions about this topic.

No. Voice calls get an end-of-call-report webhook that arrives after the call with the final transcript, recording URL and cost breakdown attached. Chat has no equivalent. This comes up repeatedly in Vapi's own community and the answer has consistently been that the event does not exist, so any design that waits for it will wait forever.

It fires, but it carries no transcript and no summary, so it tells you a chat is gone without telling you what was in it. It is also the wrong trigger conceptually: deletion is a data lifecycle event, not a conversation ending. A visitor who gets their answer and closes the tab never deletes anything.

List recent chats from the API, then fetch each one individually, because the list endpoint returns ids and timestamps without message content. Run it on a delay rather than immediately, so the conversation has actually finished before you read it. Then attach the transcript to the record you already created during the conversation.

Liquid variables do not resolve in tool fields when the assistant is running in chat mode. The dashboard renders them as pills, so they look correctly wired, and they arrive at your endpoint unresolved. The practical consequence is that tool configuration stays per-assistant with the id written in directly, rather than shared across assistants with a variable.

It is tempting and it is unreliable. Unload and visibility events are best effort, they do not fire consistently on mobile, and a visitor who closes the tab abruptly is very often exactly the lead you most wanted to keep. Anything that matters should be captured before the visitor has the chance to leave.

Nikhil Sharma

Written by

Nikhil Sharma

Founder, DigiBenders

Twelve years shipping software, five of them leading a studio in New Brunswick. I build the software and run the marketing around it, which is an unusual combination and the reason most of my work arrives by referral. One person accountable, and everything ends up in your name.

You read the thinking

Now tell me what you are actually building.

If this was useful, the call usually is too. You describe the problem, I tell you what it takes and whether I am the right person for it.

Thirty minutes, no pitch

Honest read, including when the answer is no

Replies within one business day

Book a strategy call30 min

Keep reading

More from the same desk.

What you walk away with

One instrument. You own it.

Nothing held hostage, nothing locked to a platform you cannot leave.

The codebase

Yours, in your repository

The infrastructure

Your accounts, your billing

The accounts

Registrar, analytics, ads

The documentation

Written for the next person