Clinical Integration Recipes

Short, practical recipes for the Wave-2 clinical surfaces. All snippets use @dentolize/plugin-sdk ≥ 0.3.0 (const client = new Dentolize({ token })).

Dental lab network (lab orders + webhooks)

Scopes: laborders:read, laborders:write, webhooks:manage. Events: laborder.created, laborder.updated.

Sync open orders into the lab's queue, then push status back when work completes:

// Ingest: subscribe to laborder.created, then fetch details on delivery
const order = await client.labOrders.get(event.data.id);
const items = await client.labItems.list({ labId: order.labId ?? undefined });

// Poll fallback / backfill (delta sync)
for await (const o of client.labOrders.iterate({ status: 'pending', updatedAfter: lastSync })) {
  enqueue(o);
}

// Push back: mark received when the case returns from the lab
await client.labOrders.update(order.id, { received: true, shade: 'A2', details: 'Zirconia crown, glazed' });

PATCH /lab-orders/{id} accepts only received/delivered/shade/details — a lab integration can never touch pricing. The clinic sees laborder.received / laborder.delivered fire for its own automations.

Imaging device / PACS bridge (file upload flow)

Scopes: files:read, files:write. Events fired: file.created, xray.created.

Push captured images straight onto the patient chart with the two-step upload (presign → S3 POST → confirm) — upload() does all three:

import { readFile } from 'node:fs/promises';

const file = await client.patientFiles.upload({
  patientId,
  buffer: await readFile('capture.dcm'),
  fileName: 'capture.dcm',
  contentType: 'application/dicom',
  kind: 'xray',            // also creates the x-ray record on the chart
  operationId: treatmentId // optional: attach to the treatment being imaged
});
console.log(file.xrayId, file.downloadUrl); // downloadUrl = 1h presigned GET

Allowed types: jpeg/png/webp/tiff/bmp/pdf/dicom, ≤ 100 MB. To pull existing imaging instead, use client.patientFiles.xrays(patientId) / .list(patientId).

Intake forms (external form submissions)

Scopes: forms:read, forms:write. Event fired: form.submitted.

Render the clinic's own form schema in your kiosk/portal, then submit on the patient's behalf:

// 1. Fetch the form definition (current version schema = form-builder field array)
const forms = await client.forms.list();
const intake = forms.data.find(f => f.title === 'New Patient Intake');
renderFields(intake.currentVersion?.schema ?? []);

// 2. Submit answers keyed by field key (Idempotency-Key is automatic)
const submission = await client.formSubmissions.create({
  formId: intake.id,
  patientId,
  ref: `kiosk-${sessionId}`, // your own external reference
  answers: { allergies: 'Penicillin', smoker: false }
});

Signature-typed answers are stripped from reads (formSubmissions.get()), and consent forms (client.consentForms.list()) expose signed state only — never signature images.

Vitals device (measurements on encounters)

Scopes: encounters:read, encounters:write. Event fired: measurement.recorded.

Push device readings onto the patient's open encounter:

// Find the latest encounter for the patient
const encounters = await client.encounters.list({ patientId, limit: 1 });
const encounter = encounters.data[0];

await client.encounters.addMeasurement(encounter.id, { name: 'HEART_RATE', value: 72 });
await client.encounters.addMeasurement(encounter.id, { name: 'OXYGEN_SATURATION', value: 98 });

The platform computes trend fields (improvement, difference, normal-range status) automatically — read them back via client.encounters.get(encounter.id). Note: the measurement.recorded webhook payload carries the vital under measurementName (not name), and this event fires only for measurements recorded through this REST endpoint.

Treatment-plan follow-ups (quotations)

Scope: quotations:read (financial tier). Events: quotation.created, quotation.signed (quotation.viewed is reserved — not yet emitted).

// Unsigned estimates from the last 30 days — candidates for a follow-up nudge
const pending = await client.quotations.list({ from, to, signed: false });
for (const quote of pending.data) {
  schedule(quote.patientId, { total: quote.total, quotedTreatments: quote.operationIds.length });
}

Treatments now expose plan linkage too: Treatment.quotationId, steps[], approvalRequired/approved and preAuth (via treatments:read).