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

  1. 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_SCHEMA with any JSON Schema validator, or call validateManifest(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.

  2. Send the file to the clinic (email, shared drive — it contains no secrets).

  3. Clinic uploads it: Plugin Management → Add integration → upload/paste JSON. The server re-validates and shows the issue report on any error.

  4. 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.

  5. 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 webhooks suggestions — the clinic pastes your URLs and relays each endpoint's whsec_… signing secret,
    • the token IP allowlist, pre-filled from allowedIpRanges (editable in the Connection tab).
  6. 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