Integration Manifests (self-serve private installs)
Besides the curated Plugin Hub (reviewed by Xolize, visible to every clinic), the platform supports a second distribution path that involves no review queue: you author an integration manifest — a single JSON file describing your plugin — and hand it to your customer clinic. The clinic uploads it in Plugin Management → Add integration, walks the standard consent wizard, and your plugin is installed privately for that clinic only. Zero Xolize involvement.
Use manifests for bespoke/one-clinic integrations, agency work, and pilots; graduate to a curated listing (see Listing review) when you want hub-wide distribution.
What a manifest is
A manifest is the machine-readable equivalent of a curated listing: identity, the scopes and webhook events you request, an optional settings form, suggested webhook endpoints, and an optional IP allowlist. It contains no consent language — the consent screens always render the platform's own scope/event descriptions, so a manifest cannot misrepresent what it accesses.
{
"manifestVersion": 1,
"plugin": {
"slug": "acme-agent",
"name": { "en": "Acme Booking Agent", "ar": "وكيل أكمي للحجوزات" },
"description": { "en": "WhatsApp booking bot: availability, bookings, visit invoices." },
"developerName": "Acme Inc",
"developerUrl": "https://acme.example",
"supportEmail": "support@acme.example",
"privacyPolicyUrl": "https://acme.example/privacy",
"iconUrl": "https://acme.example/icon.png"
},
"requestedScopes": ["patients:read", "appointments:write", "conversations:write", "invoices:write"],
"optionalScopes": ["loyalty:read"],
"requestedEvents": ["conversation.message_received", "appointment.created", "invoice.paid"],
"settingsSchema": { "version": 1, "sections": [] },
"webhooks": [
{ "name": { "en": "Bot inbox" }, "events": ["conversation.message_received"] },
{ "name": { "en": "Billing events" }, "events": ["invoice.paid"] }
],
"allowedIpRanges": ["203.0.113.0/24", "2001:db8::/32"]
}
Field-by-field schema
The formal contract is a JSON Schema (draft 2020-12), served by the platform and shipped in the SDK as PLUGIN_MANIFEST_JSON_SCHEMA ($id: https://developers.dentolize.com/schemas/plugin-manifest-v1.json). Annotated:
| Field | Required | Rules |
|---|---|---|
manifestVersion |
yes | Literal 1. |
plugin.slug |
yes | ^[a-z][a-z0-9-]{2,29}$ (3-30 chars). Stored namespaced per clinic (pvt-<clinic>-<slug>), so two clinics can upload the same manifest and a private slug can never collide with a curated one. |
plugin.name |
yes | Localized { en, ar? }; non-empty en, ≤200 chars each. |
plugin.description |
yes | Localized; non-empty en, ≤5000 chars. |
plugin.developerName |
yes | ≤200 chars. |
plugin.developerUrl |
no | https:// URL, ≤1000 chars. |
plugin.supportEmail |
yes | Valid email, ≤200 chars. |
plugin.privacyPolicyUrl |
yes | https:// URL, ≤1000 chars — shown on the consent screen. |
plugin.iconUrl |
no | https:// URL, ≤1000 chars. |
requestedScopes |
yes | Non-empty, unique, catalog scopes only (02). All-or-nothing consent. |
optionalScopes |
no | Unique catalog scopes, disjoint from requestedScopes; per-scope toggles at consent, default OFF. |
requestedEvents |
yes | Unique, concrete event types from the catalog (04) — wildcards are rejected here (grants are always concrete). May be empty. |
settingsSchema |
no | A settings-schema v1 document (05); validated with the same validator as curated listings. |
webhooks |
no | ≤10 suggested endpoints { name, description?, events[] }. events may use family wildcards (lead.*), but every expanded type must appear in requestedEvents. The clinic fills in your endpoint URLs at install time. |
allowedIpRanges |
no | ≤50 CIDR entries (bare IPv4/IPv6 allowed, e.g. 203.0.113.7 = /32). Pre-fills the token IP allowlist; empty/omitted = any IP. |
Unknown top-level or plugin.* keys are rejected — there is no free-form extension point.
Authoring workflow
Author with the SDK.
defineManifest(...)gives full typing plus the identical validation the server runs at upload time — mistakes fail your build, not the clinic's upload:import { defineManifest } from '@dentolize/plugin-sdk'; import { writeFileSync } from 'node:fs'; const manifest = defineManifest({ /* as above */ }); writeFileSync('acme-agent.manifest.json', JSON.stringify(manifest, null, 2));Non-TypeScript stacks: validate against
PLUGIN_MANIFEST_JSON_SCHEMAwith any JSON Schema validator, or callvalidateManifest(json)from the SDK for the exact{ valid, issues[] }report (each issue is{ path, message }). The clinic UI also offers a dry-run validation before installing.Send the file to the clinic (email, shared drive — it contains no secrets).
Clinic uploads it: Plugin Management → Add integration → upload/paste JSON. The server re-validates and shows the issue report on any error.
Consent: the standard wizard — required scopes as a fixed list, optional scopes as default-off toggles, requested events, DPA acceptance. Consent language is platform-owned throughout.
You receive the connection details from the clinic:
- the API token (
dtz_live_…/dtz_test_…) — shown once to the clinic; have them place it directly into your secret manager flow, - webhook endpoint rows pre-created from your
webhookssuggestions — the clinic pastes your URLs and relays each endpoint'swhsec_…signing secret, - the token IP allowlist, pre-filled from
allowedIpRanges(editable in the Connection tab).
- the API token (
Verify:
client.ping()shows your effective scopes; Send test event exercises each webhook endpoint through the real signing pipeline.
Updates & re-consent
A manifest has no version field — the platform derives the listing version as a content hash of the JSON. Re-uploading a manifest with any change (same slug) updates the private listing in place and forces the consent wizard again: granted scopes/events are always validated against the new manifest, and the fresh consent snapshot is recorded. Each install/re-install mints a new token; previous tokens remain valid until the clinic revokes them.
Private vs curated distribution
| Manifest (private) | Curated hub | |
|---|---|---|
| Visibility | Only the uploading clinic; never in the hub | Every clinic |
| Review | None — clinic-side consent is the gate | Xolize listing review (07) |
| Who installs | The clinic uploads + consents itself | Any clinic, from the hub |
| Slug | Namespaced per clinic (pvt-…) |
Global |
| Updates | Re-upload → re-consent | Reviewed listing update; scope escalation suspends installs until re-consent |
Both paths share the same runtime: identical tokens, scopes, rate limits, webhooks, audit logging, and DPA obligations.
Security model
- Platform-owned consent. The manifest cannot inject consent text; scope/event descriptions come from the platform dictionary. A scope or event that is not in the platform catalog hard-blocks the install.
- Same validators as curated listings. Slug rules, scope/event catalogs, and the settings-schema contract are enforced by the very same server code paths.
- Private by design. A manifest install is owned by the uploading clinic (
PRIVATEvisibility); it can never surface to other clinics — the curated hub remains the only multi-clinic distribution channel. - IP allowlisting. When a token carries an allowlist, requests from any other address are rejected with
401 ip_not_allowedbefore any data access. Recommend shippingallowedIpRangeswith your egress IPs; the clinic can adjust the list at any time in the Connection tab (02). - Re-consent on change. Because the version is a content hash, no manifest change can silently widen access — the clinic always re-walks consent.
- Auditability. PII reads, webhook deliveries and consent snapshots are logged exactly as for curated plugins (compliance notes).