OSD Specification · v1.0 · bundle schema 2

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

<source>-export-<timestamp>.zip
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 binary

Section 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_versionInteger. Current bundle schema is 2. Importers should check this first and refuse versions they don't understand.
sourceThe source platform identifier, lowercase ("vagaro", "glossgenius", "square").
exported_atISO 8601 timestamp of the export run.
countsBundle-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 / phonesArrays. Phones are normalized to digits (leading + preserved). Placeholder and masked values are dropped.
birthdayOSD date (see normalization below).
gender / tags / addressAs exposed by the vendor; address carries both the raw text and parsed parts.

note

id / created_at / authorProvenance fields; id is the vendor's identifier when one exists.
titleVendor note category or type, when the vendor has one.
bodyThe full note text.

treatment

templateVendor 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.
bodyThe rendered text of the whole treatment entry.
asset_idsLinks to this client's assets-index.json entries (e.g. attached photos).

service / product

serviceOne record per bookable unit: name, category, description, duration, price, online_bookable. Nested vendor hierarchies are flattened.
productRetail/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 price

Dates 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 ExportWrites bundles conforming to this specification.
OSD ImportReads conforming bundles without vendor-specific code.
OSD ArchiveStores bundles unmodified for long-term backup.
OSD ReadyBuilt 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.