One protocol. Every vendor.
This page defines the OSD bundle: what an export contains, how records are shaped, and what an importer can rely on. It is the contract the reference exporter writes and any OSD importer reads.
Overview
An OSD export is a single zip archive. It has two layers, and both are vendor-neutral:
The envelope is the folder layout of the zip: a manifest, a log, and one folder per client with sections and assets. It is identical for every source platform.
The canonical records are the structured JSON payloads inside that envelope. Every client, note, treatment, service, and product is normalized into the same shapes regardless of which platform it came from. An OSD importer needs zero vendor-specific code.
Alongside the canonical records, the bundle keeps the raw captured evidence (rendered HTML and plain text of each source page). Normalization never destroys source data; if a value can't be parsed, the original text is still there.
Bundle layout
manifest.json schema_version, source, bundle-level counts
log.jsonl append-only event log of the export run
tenant/
services/ page.html, page.txt, metadata.json (records)
inventory/ page.html, page.txt, metadata.json (records)
clients/
<source>_<id>/
client.json canonical record + raw vendor identity
sections/
<section>/ page.html, page.txt, metadata.json (records)
entries/<id>/ per-entry page.html + page.txt (evidence)
assets/
assets-index.json per-asset metadata (sha256, source_url, mime, size)
files/<sha256>.<ext> content-addressed binarySection names vary by what each vendor exposes (for example profile, notes, soap, appointments, files), but every section carries the same three artifacts: rendered HTML, plain text, and a metadata.json whose records array holds canonical records.
manifest.json
| schema_version | Integer. Current bundle schema is 2. Importers should check this first and refuse versions they don't understand. |
| source | The source platform identifier, lowercase ("vagaro", "glossgenius", "square"). |
| exported_at | ISO 8601 timestamp of the export run. |
| counts | Bundle-level totals (clients, notes, assets) for progress display and sanity checks. |
Canonical record shapes
Every record has a kind discriminator. Fields that a vendor doesn't expose are null, never missing.
client
| name | { text, first, last } — the rendered name plus a best-effort split. |
| emails / phones | Arrays. Phones are normalized to digits (leading + preserved). Placeholder and masked values are dropped. |
| birthday | OSD date (see normalization below). |
| gender / tags / address | As exposed by the vendor; address carries both the raw text and parsed parts. |
note
| id / created_at / author | Provenance fields; id is the vendor's identifier when one exists. |
| title | Vendor note category or type, when the vendor has one. |
| body | The full note text. |
treatment
| template | Vendor form name (e.g. "SOAP Note", "Facial Consult"). Vendors don't share a field taxonomy, so the canonical shape is provenance plus full body text. |
| body | The rendered text of the whole treatment entry. |
| asset_ids | Links to this client's assets-index.json entries (e.g. attached photos). |
service / product
| service | One record per bookable unit: name, category, description, duration, price, online_bookable. Nested vendor hierarchies are flattened. |
| product | Retail/backbar products: name, brand, category, barcode, use, retail_price, cost_price, quantity, optional image asset link. |
Normalization: never destroy source text
Parsed values always ride alongside the original rendered text, so a value the parser can't handle degrades to text instead of disappearing:
"birthday": { "text": "Nov 19, 1969", "iso": "1969-11-19" }
"price": { "text": "$85.00", "cents": 8500, "currency": "USD" }
"duration": { "text": "1 hr 15 min", "minutes": 75 }
// Ambiguous source values keep the parsed field null by design:
"birthday": { "text": "October 3", "iso": null } // no year
"price": { "text": "$50 - $80", "cents": null } // a range, not a priceDates require an explicit four-digit year to produce an iso value. Prices that are ranges, "Varies", or "Free" keep cents: null. Importers must treat the text form as authoritative when the parsed form is null.
Assets
Binary files (photos, attachments) are content-addressed: stored once as files/<sha256>.<ext> and described in assets-index.json with their hash, source URL, MIME type, and size. Records reference assets by id, so duplicates dedupe naturally and integrity is verifiable.
Identity and dedup
The stable identity of a client in a bundle is the pair (source, source_client_id) — also visible in the client folder name clients/<source>_<id>/. Importers should dedupe on this pair so re-imports and incremental exports are idempotent.
Conformance labels
Software that works with OSD describes itself with four labels:
| OSD Export | Writes bundles conforming to this specification. |
| OSD Import | Reads conforming bundles without vendor-specific code. |
| OSD Archive | Stores bundles unmodified for long-term backup. |
| OSD Ready | Built to work with OSD-formatted records. |
See the import guide for what an importer must handle, and the contributing guide to add a new source platform to the reference exporter.
Versioning
The bundle schema version is an integer that only changes when the contract changes in a way an importer must know about. The current schema is 2. Additive changes (new optional fields, new section names) do not bump the version; importers should ignore fields they don't recognize.
