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
POSTrequires anIdempotency-Keyheader; a replay within 24h returns the stored response withIdempotent-Replay: true; a concurrent duplicate gets409 idempotency_conflict(packages/server/src/plugins/api/middleware/idempotency.js). - Financial list endpoints (
/invoices,/payments,/quotations,/claims,/online-payments,/einvoice-submissions) cap thefrom/towindow at 366 days. - Core list endpoints accept
?updatedAfter=<ISO date-time>for cheap delta syncs. - Every response carries a
Dentolize-Versionheader (currently2026-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 samepayment.created/invoice.paidevents a receptionist-entered payment fires.Idempotency-Keyis 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.BALANCEandINSURANCEpayment 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 viaonlinepayment.succeeded/.failed/.refundedwebhooks, not a synchronous response — a plugin must not also callpayments.createon 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.