Hand a text chat to the video avatar
By the end of this guide your text chat can send a customer to your website's video avatar when a conversation matters (a big order, a complaint, a first setup), with the chat so far. The avatar opens with a one-line recap ("I've read your chat about the cracked kettle lid, so let's sort it out"), so the customer never repeats themselves, and the outcome comes back to your chat tool and your webhooks.
You need: a portal with a website assistant whose public page is open, and chat escalation switched on for it: in the console, open the portal's Developers tab and switch on Hand chats to the video avatar (an owner or admin does this; it is off until they do, and comes with the Starter plan and up). The same panel connects Intercom and Zendesk and shows the addresses to paste.
How a handover works#
- Your chat tool (or your server) asks Zentiq for a link, with the chat so far.
- The customer opens the link. It works for one conversation, within 30 minutes; its token sits in the link's fragment (after
#), which browsers never send to a server, so it stays out of logs. - The avatar reads the chat as information, never as instructions: it is labelled as what people typed, anything that reads like an instruction is taken out, payment and ID numbers are masked, and facts about orders or refunds are checked the usual way (your actions) before the avatar relies on them. The page says it is an AI, as every visitor page does (Telling people it's an AI).
- When the conversation ends,
escalation.completedgoes to your webhooks with the outcome, Zendesk gets an internal note, Intercom shows it in the panel, and the visitor is offered yourreturnUrl("Back to your chat").
A link that was already used, expired or belongs to another portal starts an ordinary conversation, and the page tells the customer the chat couldn't be brought over.
1. From your own chat (the API)#
Make a key with the escalations:write scope for your chat's server (or pick the Chat escalation preset under Keys and scopes). Never call it from a browser.
// Your chat's messages so far, oldest first (at most 50 and 20,000 characters: send the newest).
const chat = [
{ role: "visitor" as const, text: "My kettle arrived with a cracked lid. Order 10423.", at: "2026-09-26T10:02:11Z" },
{ role: "bot" as const, text: "Sorry about that! I can start a return for you.", at: "2026-09-26T10:02:15Z" },
{ role: "visitor" as const, text: "I'd rather talk to someone.", at: "2026-09-26T10:03:40Z" },
];
async function handOver(conversationId: string) {
try {
const link = await zentiq.createEscalation(PORTAL, {
transcript: chat,
customer: { externalId: "cust-42", firstName: "Sam" },
reason: "a cracked kettle lid",
returnUrl: `https://help.acme.example/chat/${conversationId}`,
});
// Show link.url to the customer (a button in your chat). It works once, for 30 minutes.
return link.url;
} catch (e) {
// switched_off, door_closed or no_website_subject: carry on in the chat.
if (e instanceof ConflictError) return null;
throw e;
}
}transcript:roleisvisitor,agent(a person on your team) orbot; at most 50 turns and 20,000 characters in all, 4,000 per turn (send the newest).customer.externalIdcomes back onescalation.completedascustomerRef, so you can match the outcome to your customer;firstNamelets the avatar greet them.emailis kept with the link only and is never read out or sent back.reasonfinishes the recap "your chat about …": keep it short ("the refund on order 1042").returnUrlmust behttps. The link itself is in the answer only: Zentiq keeps a hash of its token, never the token.- Refusals:
409 conflictwithdetails.reasonswitched_off,door_closedorno_website_subject; on a plan without chat handover,403 upgrade_requiredwithdetails.reasonplan_requiredanddetails.plan;404for a portal that isn't yours;410for an archived one. A retry makes a new link.
2. Get the outcome back#
const hook = await zentiq.createWebhook({ url: "https://help.acme.example/zentiq/webhooks", events: ["escalation.completed"] });function onDelivery(rawBody: string, headers: Record<string, string>) {
const event: ZentiqWebhookEvent = verifyWebhook(rawBody, headers, hook.secret);
if (event.type === "escalation.completed") {
const { customerRef, outcome, resolved, reference, minutes } = event.data;
// outcome: resolved, handoff, booking, lead, unresolved or no_conversation. A hand-off, a booking request or a lead
// is in your inbox too (lead.captured, booking.requested, handoff.requested).
console.log(customerRef, outcome, resolved, reference, minutes);
}
}outcome is one of resolved, handoff, booking, lead, unresolved and no_conversation; a hand-off, a booking request or a lead is in your inbox as well. The event carries where the chat came from (source), your chat tool's ids (externalRef, customerRef), how long it lasted and the conversation's reference. A summary of the conversation goes only to a webhook made with includeConversation, and never from a conversation whose words may not leave (a young person's, or one flagged for safety): see What leaves Zentiq and the event catalogue.
3. Intercom#
A Video assistant panel in the conversation, with a Talk to our video assistant button for your team:
- In Intercom's Developer Hub, create an app and turn on Canvas Kit for the Inbox.
- Give it the Initialize URL and Submit URL from the portal's Developers tab.
- Paste the app's client secret into the same panel (and, if you like, your Intercom workspace id, so calls from any other workspace are refused).
Every request Intercom sends is checked against that secret (its X-Body-Signature); each drawn button makes one link at most, and only for an hour after it was drawn. The panel shows the link to send in your reply, and later how the video conversation went. It reads the conversation's messages, never your internal notes.
4. Zendesk#
The link reaches the customer as a public reply on the ticket; how the conversation went comes back as an internal note (never the conversation's words).
- In Admin Center, add a webhook: the Webhook address from the portal's Developers tab, method
POST, format JSON. - Add a trigger: Tags contains
zentiq_video, then Notify webhook with the body{ "ticketId": "{{ticket.id}}" }, and Remove tagszentiq_video. - Give your agents a way to add the tag: the Zentiq app for the ticket sidebar (upload it as a private app: its files are
manifest.json,assets/iframe.html,assets/logo.png,assets/logo-small.pngandtranslations/en.json, underhttps://app.zentiqai.com/integrations/zendesk/), or a macro. - Paste the webhook's signing secret, and an agent's email and API token, into the Developers tab. Zentiq checks them with Zendesk before saving.
Zentiq takes a call only with a valid X-Zendesk-Webhook-Signature, a timestamp within 5 minutes and a signature it hasn't seen before. It reads the ticket's public comments, never internal notes, and calls only your own <subdomain>.zendesk.com.
5. A chat on your own website page#
On a page that has your website widget, your chat's own code can hand the visitor to the avatar in the widget's panel:
ZentiqWidget.escalate({
transcript: [
{ role: "visitor", text: "My kettle arrived with a cracked lid." },
{ role: "bot", text: "Sorry about that! I can start a return for you." },
],
reason: "a cracked kettle lid",
customer: { firstName: "Sam" },
}).then(
(link) => console.log("Handed over", link.id),
(error) => console.log("Stay in the chat:", error.message),
);It works only on the websites the widget may appear on. A web page isn't trusted with who the customer is: only a first name is taken from it. Nor with what your team said: the avatar reads a page's team member and bot lines as unverified, never as a commitment, and never repeats a price, refund, discount or promise from them (it checks through your actions or offers a person). To pass your own customer id (for customerRef), or a chat your server vouches for, make the link from your server with the API instead.
Secrets and limits#
- Intercom's client secret, Zendesk's signing secret and API token are encrypted at rest and never shown again, to anyone; the console shows only whether each is connected and when it was last heard from. Disconnect deletes them.
- Links: one conversation each, 30 minutes, hashed at rest; the chat is encrypted and deleted when the conversation ends or the link expires.
- Try it first: Try a test handover in the Developers tab opens your private preview with a made-up chat. A test sends nothing to your chat tools or webhooks.