Skip to the content
Menu: Website leads to your CRM

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:

crm-leads.ts
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#

crm-leads.ts
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#

crm-leads.ts
// 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#

crm-leads.ts
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:

crm-leads.ts
// 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:

crm-leads.ts
// 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);
  }
}