{
  "$schema": "./_mapping.schema.json",
  "provider": "zenoti",
  "name": "Zenoti",
  "vertical": "wellness",
  "verified_on": "2026-10-13",
  "docs": [
    {
      "title": "Zenoti API reference (index)",
      "url": "https://docs.zenoti.com/llms.txt",
      "note": "Public readme.io site; every page is also served as Markdown with the embedded OpenAPI by appending .md."
    },
    {
      "title": "Authentication",
      "url": "https://docs.zenoti.com/docs/authentication"
    },
    {
      "title": "List all centers",
      "url": "https://docs.zenoti.com/reference/list-all-centers"
    },
    {
      "title": "Retrieve a center",
      "url": "https://docs.zenoti.com/reference/retrieve-a-center"
    },
    {
      "title": "List all services of a center",
      "url": "https://docs.zenoti.com/reference/list-all-services-of-a-center"
    },
    {
      "title": "Create a service booking",
      "url": "https://docs.zenoti.com/reference/create-a-service-booking-copy"
    },
    {
      "title": "Retrieve available slots for a service booking",
      "url": "https://docs.zenoti.com/reference/retrieve-available-slots-for-a-service-booking"
    },
    {
      "title": "Confirm a service booking",
      "url": "https://docs.zenoti.com/reference/confirm-a-service-booking"
    },
    {
      "title": "Retrieve the list of appointments of a center",
      "url": "https://docs.zenoti.com/reference/retrieve-the-list-of-appointments-of-a-center"
    },
    {
      "title": "Retrieve the schedules of classes in multiple centers",
      "url": "https://docs.zenoti.com/reference/retrieve-the-schedules-of-classes-in-multiple-centers"
    },
    {
      "title": "Webhooks overview",
      "url": "https://docs.zenoti.com/docs/overview-1"
    },
    {
      "title": "Structure of event responses",
      "url": "https://docs.zenoti.com/docs/webhooks-event-response-structure"
    },
    {
      "title": "Webhook events",
      "url": "https://docs.zenoti.com/reference/webhook-events"
    }
  ],
  "access": {
    "public_docs": true,
    "how_to_get_access": "The business (org admin) creates a backend app in Zenoti under Admin > Setup > Apps and generates an API key. Webhooks additionally need the Zenoti API package enabled by the business's CSM.",
    "auth": "Authorization: apikey <api_key> (server-side), or a user access token from the Generate an access token API (https://docs.zenoti.com/reference/generate-an-access-token)",
    "base_url": "https://api.zenoti.com"
  },
  "splitclub": {
    "integration_provider": null,
    "recommended_path": "poll_and_post",
    "note": "SplitClub's inbound hook does not accept 'zenoti', and Zenoti does not document its webhook signature header or algorithm, so the 'custom' path could not authenticate deliveries. Poll services/centers/class sessions nightly and appointments every few minutes (GET /v1/appointments by center and date), then POST /v1/offerings and POST /v1/checkouts. A partner-server relay for AppointmentGroup.Created is possible once Zenoti confirms the signature scheme. Pointers are rooted at the SplitClub Offering (tiers and availability inline), Event, or checkout_create."
  },
  "objects": {
    "Offering": {
      "source": "GET /v1/Centers/{center_id}/services (services[])",
      "kind": "spa_treatment",
      "source_url": "https://docs.zenoti.com/reference/list-all-services-of-a-center",
      "fields": [
        {
          "to": "/external_id",
          "from": "services[].id",
          "transform": "GUID string, prefix zn_svc_",
          "source_url": "https://docs.zenoti.com/reference/list-all-services-of-a-center"
        },
        {
          "to": "/kind",
          "from": "const:spa_treatment",
          "source_url": "https://docs.zenoti.com/reference/list-all-services-of-a-center"
        },
        {
          "to": "/title",
          "from": "services[].name",
          "source_url": "https://docs.zenoti.com/reference/list-all-services-of-a-center"
        },
        {
          "to": "/description",
          "from": "services[].description",
          "source_url": "https://docs.zenoti.com/reference/list-all-services-of-a-center"
        },
        {
          "to": "/metadata/duration_minutes",
          "from": "services[].duration",
          "transform": "integer minutes → string (Offering has no duration field)",
          "source_url": "https://docs.zenoti.com/reference/list-all-services-of-a-center"
        },
        {
          "to": "/metadata/service_code",
          "from": "services[].code",
          "source_url": "https://docs.zenoti.com/reference/list-all-services-of-a-center"
        },
        {
          "to": "/media/0/url",
          "from": "services[].image_paths",
          "transform": "returned only with expand; its shape is not documented",
          "source_url": "https://docs.zenoti.com/reference/list-all-services-of-a-center",
          "unverified": true
        },
        {
          "to": "/metadata/is_couple_service",
          "from": "services[].is_couple_service",
          "transform": "boolean → string; couple services fit party_size 2",
          "source_url": "https://docs.zenoti.com/reference/list-all-services-of-a-center"
        }
      ]
    },
    "PriceTier": {
      "source": "GET /v1/Centers/{center_id}/services (services[].price_info)",
      "source_url": "https://docs.zenoti.com/reference/list-all-services-of-a-center",
      "note": "Pointers are inside the Offering: /tiers/N. One tier per service at its center price; therapist-level pricing and price scaling are a separate endpoint (retrieve-therapist-pricing-and-price-scaling…).",
      "fields": [
        {
          "to": "/tiers/0/external_id",
          "from": "services[].id + center_id",
          "transform": "zn_px_{center_id}_{service id}",
          "source_url": "https://docs.zenoti.com/reference/list-all-services-of-a-center"
        },
        {
          "to": "/tiers/0/label",
          "from": "services[].name",
          "source_url": "https://docs.zenoti.com/reference/list-all-services-of-a-center"
        },
        {
          "to": "/tiers/0/amount",
          "from": "services[].price_info.final_price (fallback sale_price)",
          "transform": "decimal major units (dollars) → integer cents (round half up). Zenoti's examples mix whole numbers (310) and decimals (488.5) in the same money fields",
          "source_url": "https://docs.zenoti.com/reference/list-all-services-of-a-center"
        },
        {
          "to": "/tiers/0/currency",
          "from": "centers[].currency.code (GET /v1/centers)",
          "transform": "e.g. USD → usd. price_info only carries a numeric currency_id",
          "source_url": "https://docs.zenoti.com/reference/list-all-centers"
        },
        {
          "to": "/tiers/0/unit",
          "from": "const:per_person",
          "transform": "a service is booked per guest",
          "source_url": "https://docs.zenoti.com/reference/list-all-services-of-a-center"
        },
        {
          "to": "/tiers/0/fees/0/amount",
          "from": "services[].price_info.tax",
          "transform": "decimal major units (dollars) → integer cents (round half up). Zenoti's examples mix whole numbers (310) and decimals (488.5) in the same money fields; type=tax, included = price_info.include_tax",
          "source_url": "https://docs.zenoti.com/reference/list-all-services-of-a-center"
        }
      ]
    },
    "Availability": {
      "source": "POST /v1/bookings (center_id, date, guests[].items[].item.id) then GET /v1/bookings/{booking_id}/slots (slots[])",
      "source_url": "https://docs.zenoti.com/reference/retrieve-available-slots-for-a-service-booking",
      "note": "Pointers are inside the Offering: /availability. Zenoti has no catalogue-level availability feed: slots exist only for a booking draft (center, date, guest, service). Poll by creating a draft per service and date. Slot end = Time + services[].duration.",
      "fields": [
        {
          "to": "/availability/timezone",
          "from": "centers[].location.time_zone.standard_name",
          "transform": "Windows zone name (e.g. 'Pacific Standard Time') → IANA via the CLDR windowsZones table",
          "source_url": "https://docs.zenoti.com/reference/list-all-centers"
        },
        {
          "to": "/availability/slots/0/starts_at",
          "from": "slots[].Time",
          "transform": "center-local wall-clock time without offset → add the offset of the center's time zone (centers[].location.time_zone) for that date (the example values carry no offset; the docs do not state the zone explicitly)",
          "source_url": "https://docs.zenoti.com/reference/retrieve-available-slots-for-a-service-booking",
          "unverified": true
        },
        {
          "to": "/availability/slots/0/ends_at",
          "from": "slots[].Time + services[].duration",
          "transform": "center-local wall-clock time without offset → add the offset of the center's time zone (centers[].location.time_zone) for that date",
          "source_url": "https://docs.zenoti.com/reference/list-all-services-of-a-center"
        },
        {
          "to": "/availability/slots/0/capacity",
          "from": "slots[].Available",
          "transform": "true → 1, false → 0 (or drop the slot)",
          "source_url": "https://docs.zenoti.com/reference/retrieve-available-slots-for-a-service-booking"
        },
        {
          "to": "/availability/slots/0/tier_overrides/0/amount",
          "from": "slots[].SalePrice",
          "transform": "null in every example; type undocumented. When set, decimal major units (dollars) → integer cents (round half up). Zenoti's examples mix whole numbers (310) and decimals (488.5) in the same money fields",
          "source_url": "https://docs.zenoti.com/reference/retrieve-available-slots-for-a-service-booking",
          "unverified": true
        }
      ]
    },
    "Event": {
      "source": "GET /v1/classes/sessions?center_id=…&start_date=…&end_date=… (sessions[])",
      "source_url": "https://docs.zenoti.com/reference/retrieve-the-schedules-of-classes-in-multiple-centers",
      "fields": [
        {
          "to": "/external_id",
          "from": "sessions[].id",
          "transform": "integer → string, prefix zn_ses_",
          "source_url": "https://docs.zenoti.com/reference/retrieve-the-schedules-of-classes-in-multiple-centers"
        },
        {
          "to": "/offering_external_id",
          "from": "sessions[].class_id",
          "transform": "prefix zn_cls_ (class Offering from classes[].id / classes[].name, kind=class)",
          "source_url": "https://docs.zenoti.com/reference/retrieve-the-schedules-of-classes-in-multiple-centers"
        },
        {
          "to": "/title",
          "from": "sessions[].name",
          "source_url": "https://docs.zenoti.com/reference/retrieve-the-schedules-of-classes-in-multiple-centers"
        },
        {
          "to": "/description",
          "from": "sessions[].description",
          "source_url": "https://docs.zenoti.com/reference/retrieve-the-schedules-of-classes-in-multiple-centers"
        },
        {
          "to": "/starts_at",
          "from": "sessions[].start_time_utc (fallback start_time)",
          "transform": "start_time_utc → append Z then re-express with the center offset; start_time is center-local without offset",
          "source_url": "https://docs.zenoti.com/reference/retrieve-the-schedules-of-classes-in-multiple-centers"
        },
        {
          "to": "/ends_at",
          "from": "sessions[].end_time_utc (fallback end_time)",
          "transform": "as starts_at",
          "source_url": "https://docs.zenoti.com/reference/retrieve-the-schedules-of-classes-in-multiple-centers"
        },
        {
          "to": "/capacity",
          "from": "sessions[].web_capacity (fallback capacity)",
          "source_url": "https://docs.zenoti.com/reference/retrieve-the-schedules-of-classes-in-multiple-centers"
        },
        {
          "to": "/tiers/0/amount",
          "from": "sessions[].price",
          "transform": "decimal major units (dollars) → integer cents (round half up). Zenoti's examples mix whole numbers (310) and decimals (488.5) in the same money fields; unit per_person; skip when is_free_session",
          "source_url": "https://docs.zenoti.com/reference/retrieve-the-schedules-of-classes-in-multiple-centers"
        },
        {
          "to": "/metadata/available_slots",
          "from": "sessions[].available_slots",
          "transform": "integer → string; seats left",
          "source_url": "https://docs.zenoti.com/reference/retrieve-the-schedules-of-classes-in-multiple-centers"
        }
      ]
    },
    "Checkout": {
      "source": "GET /v1/appointments?center_id&start_date&end_date (appointments), or webhook AppointmentGroup.Created",
      "source_url": "https://docs.zenoti.com/reference/retrieve-the-list-of-appointments-of-a-center",
      "note": "One Zenoti appointment group (invoice) holds every guest's appointments, which matches one SplitClub checkout. The appointment payloads carry no price; amount comes from the services' price_info (or the Invoice.Closed total after the fact).",
      "fields": [
        {
          "to": "/external_booking_id",
          "from": "appointment_group_id",
          "transform": "GUID, prefix zn_ag_",
          "source_url": "https://docs.zenoti.com/reference/retrieve-the-list-of-appointments-of-a-center"
        },
        {
          "to": "/offering_external_id",
          "from": "service.id",
          "transform": "prefix zn_svc_; first service when the group mixes services",
          "source_url": "https://docs.zenoti.com/reference/retrieve-the-list-of-appointments-of-a-center"
        },
        {
          "to": "/description",
          "from": "service.name",
          "source_url": "https://docs.zenoti.com/reference/retrieve-the-list-of-appointments-of-a-center"
        },
        {
          "to": "/party_size",
          "from": "count of distinct guest.id across the group's appointments",
          "transform": "SplitClub-side aggregation over appointments sharing appointment_group_id",
          "source_url": "https://docs.zenoti.com/reference/retrieve-the-list-of-appointments-of-a-center"
        },
        {
          "to": "/amount",
          "from": "sum of services[].price_info.final_price for the group's service ids",
          "transform": "decimal major units (dollars) → integer cents (round half up). Zenoti's examples mix whole numbers (310) and decimals (488.5) in the same money fields",
          "source_url": "https://docs.zenoti.com/reference/list-all-services-of-a-center"
        },
        {
          "to": "/currency",
          "from": "centers[].currency.code",
          "transform": "lower case",
          "source_url": "https://docs.zenoti.com/reference/list-all-centers"
        },
        {
          "to": "/pay_by",
          "from": "start_time_utc (earliest in group)",
          "transform": "UTC without 'Z' in the examples → treat as UTC, minus cutoff_minutes",
          "source_url": "https://docs.zenoti.com/reference/retrieve-the-list-of-appointments-of-a-center",
          "unverified": true
        },
        {
          "to": "/metadata/zenoti_invoice_id",
          "from": "invoice_id",
          "source_url": "https://docs.zenoti.com/reference/retrieve-the-list-of-appointments-of-a-center"
        },
        {
          "to": "/metadata/zenoti_center_id",
          "from": "center_id (query parameter)",
          "source_url": "https://docs.zenoti.com/reference/retrieve-the-list-of-appointments-of-a-center"
        }
      ]
    }
  },
  "webhooks": {
    "supported": true,
    "signature": {
      "header": "not named in the docs",
      "algorithm": "not documented",
      "signed_payload": "The docs say only that the HTTP header 'contains a signature for verification and validation of the response'.",
      "source_url": "https://docs.zenoti.com/docs/webhooks-event-response-structure",
      "unverified": true
    },
    "events": [
      {
        "event": "AppointmentGroup.Created",
        "maps_to": "create Checkout (group of guest appointments)",
        "source_url": "https://docs.zenoti.com/reference/appointment-group-created",
        "fields": [
          {
            "to": "/external_booking_id",
            "from": "data.appointment_group_id",
            "source_url": "https://docs.zenoti.com/reference/appointment-group-created"
          },
          {
            "to": "/offering_external_id",
            "from": "data.appointments[].service_id",
            "source_url": "https://docs.zenoti.com/reference/appointment-group-created"
          },
          {
            "to": "/description",
            "from": "data.appointments[].service_name",
            "source_url": "https://docs.zenoti.com/reference/appointment-group-created"
          },
          {
            "to": "/pay_by",
            "from": "data.appointments[].start_time_in_center",
            "transform": "center-local, no offset → add center offset; minus cutoff_minutes. (start_time in the same payload is offset from the center time, apparently UTC without 'Z')",
            "source_url": "https://docs.zenoti.com/reference/appointment-group-created"
          },
          {
            "to": "/metadata/zenoti_invoice_id",
            "from": "data.invoice_id",
            "source_url": "https://docs.zenoti.com/reference/appointment-group-created"
          }
        ]
      },
      {
        "event": "AppointmentGroup.Status",
        "maps_to": "cancel the Checkout when the group is cancelled (status values not documented on the event page)",
        "source_url": "https://docs.zenoti.com/reference/appointment-group-status",
        "unverified": true
      },
      {
        "event": "Invoice.Closed",
        "maps_to": "reconcile: invoice paid at Zenoti; data.invoice.total_price.sum_total decimal major units (dollars) → integer cents (round half up). Zenoti's examples mix whole numbers (310) and decimals (488.5) in the same money fields",
        "source_url": "https://docs.zenoti.com/reference/invoice-closed-event"
      },
      {
        "event": "Appointment.Started",
        "maps_to": "ignore",
        "source_url": "https://docs.zenoti.com/reference/webhook-events",
        "unverified": true
      },
      {
        "event": "Appointment.Completed",
        "maps_to": "ignore (sample payload omits the envelope)",
        "source_url": "https://docs.zenoti.com/reference/appointment-completed",
        "unverified": true
      },
      {
        "event": "Class.Schedule.Created",
        "maps_to": "upsert Event: data.sesion_id (sic), class_name, start_time/end_time (center-local), capacity, web_capacity",
        "source_url": "https://docs.zenoti.com/reference/schedule-created"
      },
      {
        "event": "Class.Schedule.Updated",
        "maps_to": "upsert Event",
        "source_url": "https://docs.zenoti.com/reference/schedule-updated"
      },
      {
        "event": "Class.Schedule.Cancelled",
        "maps_to": "cancel Event",
        "source_url": "https://docs.zenoti.com/reference/schedule-cancelled"
      },
      {
        "event": "Class.Booking.Created",
        "maps_to": "create Checkout for a class seat: external_booking_id = data.registration_id, offering = data.class_id, event = data.session_id",
        "source_url": "https://docs.zenoti.com/reference/booking-created"
      },
      {
        "event": "Class.Booking.Cancelled",
        "maps_to": "cancel that seat's Checkout",
        "source_url": "https://docs.zenoti.com/reference/booking-cancelled"
      }
    ],
    "note": "Envelope: id, event_id, event_schema ('v1'), event_resource, event_type, event_timestamp (UTC), data. Webhooks are configured in the Zenoti admin UI (event + verb + URL), not by API. Retries: up to 5 with backoff (30 s to 10 min); deactivated after 20 consecutive failures. Event type strings were read from each event page's sample payload; the Appointment.Started name was not read from a payload."
  },
  "gaps": [
    "Webhook signature header and algorithm are not documented: SplitClub cannot authenticate Zenoti deliveries without asking Zenoti (via the business's CSM).",
    "No catalogue availability: slots require a booking draft per center, date, guest and service (POST /v1/bookings then GET …/slots).",
    "Times are center-local without offset in most APIs, and the center zone is a Windows name that needs conversion to IANA. Slot Time zone is inferred from examples, not stated.",
    "Appointments carry no price (price is null in the example); amount must come from service price_info or therapist pricing.",
    "Money fields are typed integer in some schemas but show decimals in payloads (488.5); treat all as decimal major units.",
    "Cancellation/no-show policy fields (sessions[].cancellation_fee, cancellation_fee_window) exist for classes only and their units are undocumented; /policy is not mapped.",
    "AppointmentGroup.Status values are not documented on the event page."
  ]
}