Growth & Operations Integration Recipes

Wave-4 surfaces: CRM leads, outbound/inbound messaging, loyalty, feedback, tasks, inventory, bookkeeping export, analytics — plus the cross-cutting /exports and /events endpoints. Scope table in 02, routes in 03, events in 04.

Ad-platform lead ingestion (Meta/TikTok/Google lead forms)

Scopes: leads:write (implies leads:read). Events: lead.*.

try {
  await client.leads.create(
    { name, phone, sourceId, subSourceId, dealSize, notes },
    { idempotencyKey: `meta-${adLeadId}` } // your platform's lead id
  );
} catch (error) {
  if (error instanceof DentolizeApiError && error.code === 'lead_exists') return; // dedupe: phone already a lead
  throw error;
}

Reminder / marketing messaging

Scopes: communications:send (+ communications:read for the delivery log). Events: message.sent / message.failed.

const queued = await client.messages.send({
  patientId, channel: 'whatsapp', templateId, variables: ['Sara', 'Sunday 9am']
});
// 202 — correlate later outcomes via queued.communicationId

Patient-facing agentic bot (WhatsApp booking + billing)

The full bot loop — event-driven, no polling. Scopes: conversations:write, patients:read (+ patients:write to register new patients, .pii to greet by name), availability:read, appointments:write, invoices:write, paymentlinks:write. Events: conversation.message_received, appointment.*, invoice.paid, onlinepayment.*.

// 1. Inbound message webhook (thin: ids only — pull the text)
//    event.type === 'conversation.message_received'
const { conversationId, messageId, patientId, leadId } = event.data;
const history = await client.conversations.messages(conversationId!, { limit: 20 });

// 2. Who is texting? Conversation linkage first, phone lookup as fallback
const conv = (await client.conversations.list({ patientId })).data[0]
  ?? (await client.conversations.list({ phone: userPhone })).data[0];
const who = patientId
  ? { registered: true, patientId }
  : await client.patients.lookup(userPhone); // { registered, patientId, leadId }
const patient = who.registered
  ? await client.patients.get(who.patientId!)
  : await client.patients.create({ branchId, firstName, lastName, phoneNumber: userPhone });

// 3. Offer slots and book
const slots = await client.availability.list({ branchId, doctorId, date: '2026-08-30' });
const appointment = await client.appointments.create({
  patientId: patient.id, branchId, doctorId, start, end
});

// 4. After the visit: issue the invoice and send a payment link
const invoice = await client.invoices.create(
  { patientId: patient.id, branchId, appointmentId: appointment.id,
    items: [{ procedureId: cleaningId }] },
  { idempotencyKey: `visit-${appointment.id}` }
);
const link = await client.paymentLinks.create({ invoiceId: invoice.id });
await client.conversations.reply(conversationId!, { text: `Pay here: ${link.paymentLink}` });
// 'invoice.paid' / 'onlinepayment.succeeded' close the loop; thank the patient there.

Loyalty / engagement platforms

Scopes: loyalty:read / loyalty:write. Events: points.earned / points.redeemed.

const balance = await client.loyalty.patientPoints(patientId); // total/used/remaining/expired
await client.loyalty.createTransaction({ patientId, points: 250, reason: 'Referral campaign' });
await client.loyalty.createTransaction({ patientId, points: -500, reason: 'Reward redemption' });

Supplier purchase-order flow

Scopes: inventory:read / inventory:write. Events: inventory.low_stock, inventoryorder.created / inventoryorder.received.

const low = await client.inventoryItems.list({ belowMin: true }); // restock candidates
const po = await client.inventoryOrders.create({
  supplierId, branchId,
  items: low.data.map(i => ({ inventoryItemId: i.id, quantity: i.preferredAmount ?? 10 }))
});
await client.inventoryOrders.updateStatus(po.id, { status: 'CONFIRMED' });
await client.inventoryOrders.updateStatus(po.id, { status: 'IN_TRANSIT' });

BI / bookkeeping bridges (QuickBooks, Xero, dashboards)

Scopes: bookkeeping:read (financial), analytics:read, plus read scopes of anything you export.

// Backfill once via bulk export (NDJSON, ≤50k rows — window with updatedAfter):
const job = await client.exports.create({ resource: 'invoices' });
const { url, rows } = await client.exports.waitFor(job.exportId, { intervalMs: 2000 });

// Then delta-sync the ledgers with updatedAfter + from/to windows (≤366 days):
for await (const expense of client.expenses.iterate({ from, to, updatedAfter: cursor })) { /* map to your CoA */ }
const kpis = await client.analytics.revenue({ from, to, branchId }); // aggregates only, never row-level PII

Checklist before listing review

  1. Required vs optional scopes split defensibly (02); degrade gracefully when optional scopes are off.
  2. Idempotency keys are your own stable ids (ad-lead id, PO number) — not random per attempt.
  3. Handle the domain 409s (lead_exists, conversation_expired, insufficient_points) as flow control, not errors.
  4. Subscribe with family wildcards (lead.*) only when the whole family is granted; dedupe deliveries by event.id.