Send website leads to your CRM
By the end of this guide an avatar on your website qualifies visitors, and every lead, booking request and hand-off it takes reaches your CRM at once, with the visitor's contact details and consent. Your CRM updates the status, and stays in sync after an outage.
You need: a key made with the Website assistant preset to build the assistant, a key made with the CRM preset for your CRM (keys made before 2026-10-01 reach the inbox only once an owner adds inbox:read and inbox:write), and the Node SDK.
1. Put a lead-qualifier on your website#
Its content (the questions to qualify a lead, your FAQs, your departments), published, and its visitor door opened for your own website only:
const desk = await zentiq.createPortal({ name: "Acme Sales Desk", templates: ["lead-qualifier"] });
const subjectId = desk.subjects[0].id;
await zentiq.updateSubjectContent(subjectId, {
set: {
qualification: [{ question: "When would you like to start?", field: "timeline", why: "The team calls the soonest first." }],
faqs: [{ q: "Do you offer a free trial?", a: "Yes, fourteen days, no card needed." }],
departments: [{ name: "Sales", handles: "New customers", email: "sales@acme.example" }],
},
});
await zentiq.publishSubject(subjectId);
await zentiq.publishPortal(desk.id);
// Open the visitor door, for your own website only.
await zentiq.updateVisitorDoor(desk.id, { allowedOrigins: [SITE], open: true });2. Register your CRM's webhook#
const hook = await zentiq.createWebhook({
url: "https://crm.acme.example/zentiq/webhooks",
events: ["lead.captured", "booking.requested", "handoff.requested", "lead.updated", "visitor.detail_removed"],
});3. Receive leads#
// In your receiver (see "Receiving webhooks"): a lead carries the visitor's contact details and their consent.
function onDelivery(rawBody: string, headers: Record<string, string>) {
const event: ZentiqWebhookEvent = verifyWebhook(rawBody, headers, hook.secret);
if (event.type === "lead.captured") {
const { inboxItemId, contact, consent, leadBand } = event.data;
// consent.purpose is "callback": call them back about this request; consent.marketing says whether you may market to them.
console.log(inboxItemId, contact?.name, contact?.email, contact?.phone, leadBand, consent?.marketing);
}
if (event.type === "visitor.detail_removed") {
// The visitor asked Zentiq to forget some details: delete the same fields from your CRM.
console.log(event.data.sessionId, event.data.fields);
}
}A lead carries the visitor's contact details (only what they gave, with their consent) and the consent itself: purpose says what they agreed to be contacted about, marketing whether they agreed to more. What the visitor said in their own words goes only to a webhook made with includeConversation. An under-18 visitor's details are never taken down. See The inbox.
4. Move it on#
async function markContacted(inboxItemId: string) {
try {
// expectedStatus: only if nobody moved it on meanwhile.
await zentiq.updateInboxItem(inboxItemId, { status: "contacted", expectedStatus: "new" });
} catch (e) {
if (e instanceof ConflictError) console.log("Someone else moved it on first: read it again.");
else throw e;
}
}lead.updated then says who moved it (by: your key, or a member in the console).
5. Check deliveries#
If your CRM is down, Zentiq keeps retrying for about two days. The delivery log shows what failed and when the next attempt is:
// Deliveries that failed, and when the next attempt is.
const failed = await zentiq.webhookDeliveries({ webhookId: hook.id, ok: false });
for (const d of failed.data) console.log(d.id, d.eventId, d.state, d.status, d.nextAttemptAt);6. Catch up#
After an outage (or every night, to be sure), ask for everything since your last sync, oldest first:
// After an outage, or every night: what changed since your last sync, oldest first.
async function reconcile(since: string) {
for await (const item of zentiq.paginate("listInboxItems", { updatedAfter: since, order: "asc", portalId: desk.id })) {
console.log(item.id, item.status, item.contact?.email);
}
// The event log has every event your webhooks were offered, with the same ids.
for await (const event of zentiq.paginate("listEvents", { type: "lead.captured", from: since, order: "asc" })) {
console.log(event.id, event.createdAt);
}
}