Dentolize · Plugin Platform Walkthrough
On this pageBusiness viewTechnical view

REST API & SDK

Business view

Once a plugin is installed, its backend talks to Dentolize the same way any modern SaaS integration works: a versioned REST API, authenticated with a bearer token, returning JSON. A partner developer doesn't need to understand Dentolize's internal data model — they get a curated set of ~30 resources (patients, appointments, invoices, lab orders, quotations, claims, and more), each scoped to exactly what their installation was granted.

To make this approachable, Xolize ships an official Node/TypeScript client, @dentolize/plugin-sdk, so a partner can write:

import { Dentolize } from '@dentolize/plugin-sdk'
const client = new Dentolize({ token: process.env.DENTOLIZE_TOKEN })
const clinic = await client.clinic.get()
for await (const appt of client.appointments.iterate({ from: '2026-07-01', to: '2026-07-31' })) {
  console.log(appt.id, appt.status)
}

instead of hand-rolling pagination, retries, and rate-limit backoff. The SDK package includes a full worked example app (examples/appointment-reminder-bot) that a new partner can literally run.

Technical view

Conventions (docs/plugin-platform/03-rest-api.md, verified against packages/server/src/plugins/api/)

  • Base URL https://<host>/api/v1; Authorization: Bearer dtz_live_… / dtz_test_….
  • Detail/write responses wrap the resource ({ "data": {...} }); lists return { "data": [...], "hasMore", "nextCursor" }, paged via ?cursor=&limit= (≤100, default 25).
  • Errors are a consistent envelope: { "error": { "code", "message", "requestId", "details?" } } (packages/server/src/plugins/api/middleware/errorHandler.js).
  • Every POST requires an Idempotency-Key header; a replay within 24h returns the stored response with Idempotent-Replay: true; a concurrent duplicate gets 409 idempotency_conflict (packages/server/src/plugins/api/middleware/idempotency.js).
  • Financial list endpoints (/invoices, /payments, /quotations, /claims, /online-payments, /einvoice-submissions) cap the from/to window at 366 days.
  • Core list endpoints accept ?updatedAfter=<ISO date-time> for cheap delta syncs.
  • Every response carries a Dentolize-Version header (currently 2026-07); the API is versioned by URL and additive-only changes don't bump it.

The full resource surface

30 controllers under packages/server/src/plugins/api/controllers/ (patients, appointments, availability, invoices, payments, treatments, webhooks, clinic, branches, practitioners, procedures, reference, installation, labs, quotations, prescriptions, forms, encounters, files, insurance, claims, onlinePayments, paymentLinks, einvoiceSubmissions, leads, communications, conversations, loyalty, feedback, tasks), each paired with an input-validation schema in .../api/schemas/ and a whitelist-only response serializer in .../api/serializers/. The full machine-readable contract is generated to packages/plugin-sdk/openapi/dentolize-v1.json (regenerated via yarn workspace @dentolize/server openapi:generate, packages/server/scripts/generateOpenApi.js) — a 12,800-line OpenAPI 3.1 document; render it with any OpenAPI viewer (the roadmap notes the published docs site plans a Scalar static build).

Two write paths deserve special mention because they reuse real platform business logic rather than writing a parallel "plugin version" of it:

  • POST /payments (payments:write) reuses the full internal payment core — treasury routing via branch defaults, invoice/patient total recalculation, the same payment.created/invoice.paid events a receptionist-entered payment fires. Idempotency-Key is mandatory and the payment id is additionally derived from the key, so a retry that somehow slips past the 24h replay cache still can't double-charge — it collides on id instead. BALANCE and INSURANCE payment types are rejected (not reachable from the API).
  • POST /payment-links (paymentlinks:write) creates a hosted payment page on the clinic's own configured gateway; outcomes arrive via onlinepayment.succeeded/.failed/.refunded webhooks, not a synchronous response — a plugin must not also call payments.create on success, the platform already recorded it.

File uploads are a three-step presigned flow

POST /patients/:id/files returns a presigned S3 POST target; the caller multipart-POSTs the file directly to S3 (no Dentolize auth headers on that request, file field must be last); then POST /patients/:id/files/:fileId/confirm finalizes the record and fires file.created/xray.created. The SDK's client.patientFiles.upload() does all three steps as one call (packages/plugin-sdk/src/resources.ts).

The SDK package (packages/plugin-sdk/)

TypeScript, published as @dentolize/plugin-sdk. Structure: client.ts (the Dentolize class, auth + base config), http.ts (fetch wrapper with retry/backoff on 429), resources.ts (1,111 lines — one namespace per resource, list/get/create/update/iterate methods, iterate() being an async generator that pages automatically), webhooks.ts (constructEvent() for signature verification), settings.ts / settingsValidation.ts (client-side mirrors of the settings-schema contract, importable as defineSettingsSchema()), scopes.ts (the scope catalog, ALL_SCOPES/SCOPES — a TS mirror of the server's scopes.js), errors.ts (typed error classes per error.code), pagination.ts. A scripts/smoke.mjs (947 lines) end-to-end exercises the SDK against a running server — useful as a reference for "what does a full integration touch."

The SDK automatically respects Retry-After on 429s, so a well-behaved partner doesn't need to hand-implement backoff — see Sandbox & compliance for the exact rate-limit numbers.