{
  "openapi": "3.1.0",
  "info": {
    "title": "Easy Appointment Booking API",
    "version": "1.6.0",
    "description": "The Easy Appointment Booking API lets you work with a merchant's events, locations, staff schedules, bookings, and reports. Every API key belongs to one Shopify shop, so it can access only that shop's data.\n\n**Base URL:** `https://app.getservicify.com/api/public/v1`\n\n> **This API has no sandbox or test mode.** Read requests return live shop data. Write requests change live data and can trigger customer emails or text messages, calendar updates, webhooks, refunds, and connected integrations. The interactive request button is disabled for this reason.\n\n## Get access and create a key\n\nAPI access must be enabled for the shop. In the Easy Appointment Booking admin, open **Settings > API access**. If API access has not been enabled for the shop, use the in-app chat to request it.\n\nCreate a separate key for each integration and grant only the scopes it needs. The full key is shown once. Store it as a server-side secret; do not put it in browser code, mobile apps, public repositories, or logs. Revoking a key stops it from working immediately.\n\nSend the key as a bearer token on every protected request:\n\n```http\nAuthorization: Bearer srv_live_...\n```\n\n### Available scopes\n\n| Scope | Allows |\n| --- | --- |\n| `events:read` | List events, read booking options, and find bookable slots |\n| `events:write` | Create, update, and duplicate events, and set assigned staff |\n| `bookings:read` | List bookings and retrieve one complete booking |\n| `bookings:create` | Create bookings |\n| `bookings:write` | Update, assign, reschedule, cancel, and remind bookings |\n| `availability:read` | List staff profiles and schedules |\n| `availability:write` | Create staff profiles and replace staff schedules |\n| `locations:read` | List locations and retrieve location details |\n| `locations:write` | Create locations, update their details, and map them to Shopify locations |\n| `reports:read` | Read the peak-hours report |\n| `waitlist:read` | List and retrieve waitlist entries and counts |\n| `waitlist:write` | Join, update, and send invitation messages for waitlist entries |\n\n## Make your first request\n\nStart with `GET /me`. It verifies the key and tells you which shop and scopes you are using. It also returns the server time.\n\n```bash\ncurl https://app.getservicify.com/api/public/v1/me \\\n  -H \"Authorization: Bearer $SERVICIFY_API_KEY\"\n```\n\nA valid response includes the shop domain, the credential name and scopes, the shop's timezone, `serverTime`, and `apiVersion`. If this request fails, check the key before calling another endpoint. Review the returned scopes before choosing your next call.\n\n## Create a booking\n\nCreating a booking and reading its saved time is a five-step flow. This flow reserves a place without collecting payment. Do not build a booking request from event fields alone.\n\n1. **Choose an event:** Call `GET /events` and keep the event's `id`. Use an active event with `shouldGateBooking: false` for a customer API flow.\n2. **Read its booking options:** Call `GET /events/{eventId}/booking-options`. Choose a valid variant and quantity, and collect any required custom-field answers.\n3. **Find a live slot:** Call `GET /events/{eventId}/slots` with `from`, `to`, and the intended `variantId` and `quantity`. Choose one returned slot.\n4. **Create the booking:** Send the slot's `startAt`, `availabilityId`, and `variantId` to `POST /bookings`, along with the same quantity, the customer, and the required answers. Send a unique `Idempotency-Key` header.\n5. **Read the saved time:** Call `GET /bookings/{id}` with the created ID and use its `date` for calendar confirmation. This requires `bookings:read`.\n\nThe API checks the slot and its capacity again during creation. A slot can disappear between steps if another customer books it. Use the error-code recovery table below; only slot and capacity conflicts call for a new slot selection.\n\n`sendConfirmation` defaults to `true`. Set it to `false` to skip the confirmation email and SMS. Calendar updates, webhooks, and integrations still follow the merchant's settings.\n\n## Creation receipt\n\nA `201` response means the reservation was saved. It returns the booking ID and `eventDateId`, but does not guarantee a `date` object or top-level `startAt`/`endAt`. Call `GET /bookings/{id}` with the returned ID before you build a calendar confirmation. Use `date.utcStartTimeV2` and `date.utcEndTimeV2` from that response as the saved start and end times. This read requires `bookings:read`; grant it together with `events:read` and `bookings:create` when you set up the integration. If the read fails, retry the read with the saved ID. Do not create another booking.\n\n## Custom fields and answers\n\nRead `booking-options.customFields` in the returned order. Use each field's `inputType` to choose a control. The supported types and string answer formats are:\n\n| `inputType` | Control and `properties` | `answers[].value` example |\n| --- | --- | --- |\n| `text` | Text input. `textType: \"single\"` is one line; `textType: \"multi\"` is a text area. Missing `textType` means `single`. | `\"Dietary notes\"` |\n| `date` | Date input. No type-specific properties. | `\"2026-08-20\"` (local calendar date, `YYYY-MM-DD`, no timezone) |\n| `dropdown` | Select one entry from `properties.options`, an array of strings. | `\"Vegetarian\"` (the exact option text) |\n| `multiple_choice` | Select zero or more entries from `properties.options`, an array of strings. | `\"Vegetarian,Gluten free\"` (selected option texts joined with a comma) |\n| `checkbox` | Confirmation checkbox. No type-specific properties. | `\"Yes\"` when checked, `\"No\"` when unchecked |\n| `signature` | Signature drawing control. No type-specific properties. | `\"data:image/png;base64,...\"` (a PNG data URL from the completed drawing) |\n\n`properties` can be null or absent. Ignore unknown property keys. For a choice field with no options, or an unknown input type, ask the merchant to correct the field before offering the booking. Multiple-choice values use a legacy comma-separated format with no escaping rule. Do not invent a JSON-array format for option labels that contain commas; use the standard booking widget or ask the merchant to use labels without commas.\n\nSend string answers in these formats. The server trims leading and trailing whitespace. Its required-answer check detects missing or empty answers; it does not fully validate each control's format or option membership. Validate dates, option membership, and signatures in your integration. A required checkbox must be checked in your form, even though the server's non-empty check also accepts `\"No\"`.\n\nOmit unanswered optional fields. For each required per-attendee field, send one answer for every `attendeeIndex` from 1 through `quantity`. Omit `attendeeIndex` for fields asked once per booking. Use each `(fieldId, attendeeIndex)` pair at most once. For example, two attendees answering field 301 use `[{\"fieldId\":301,\"attendeeIndex\":1,\"value\":\"Ada\"},{\"fieldId\":301,\"attendeeIndex\":2,\"value\":\"Grace\"}]`.\n\nSend signature image data when creating a booking, not a `signature:<id>` copied from another booking. Stored answers can contain a `signature:<id>` reference instead of the original image; v1 has no public signature-image read endpoint.\n\n## Prices and currency\n\n`BookingVariant.price` is a decimal string in the shop's default currency, not minor units. For example, `\"75.00\"` means 75 currency units. It is catalog metadata and can be absent or null; it is not a checkout quote.\n\nNeither `/me` nor `booking-options` returns a currency code in v1. Read the same shop's ISO 4217 currency code from Shopify's Admin GraphQL API with `query { shop { myshopifyDomain currencyCode } }`. Match `myshopifyDomain` to the `shop` returned by `/me`. This requires a separate Shopify Admin API access token; an Easy Appointment Booking key cannot authenticate to Shopify. See [Shop.currencyCode](https://shopify.dev/docs/api/admin-graphql/latest/objects/Shop#field-Shop.fields.currencyCode) and [ProductVariant.price](https://shopify.dev/docs/api/admin-graphql/latest/objects/ProductVariant#field-ProductVariant.fields.price).\n\nIf your integration has no Shopify API access, have the merchant supply and maintain the default shop currency as integration configuration. Do not infer it from the customer's locale, the shop's timezone, or a currency symbol. If you cannot establish the currency, omit the formatted price. For Shopify Markets prices, obtain the price amount and its currency together from Shopify in the customer's market context. Do not combine a market currency with `BookingVariant.price`.\n\n## Paid bookings\n\nPublic API v1 has no end-to-end paid-checkout flow. `POST /bookings` creates a durable reservation through the manual-booking path. It does not collect payment, return a checkout URL, or create an expiring checkout hold. The prepared price is zero. A merchant setting can create a Shopify draft order and set its price, but v1 does not expose invoice sending or payment completion for that draft. A successful booking response or a zero price is not proof of payment.\n\nThe supported customer payment flow uses the Easy Appointment Booking widget on the merchant's Shopify product page:\n\n1. Open the product page with the booking widget enabled.\n2. Let the customer select a time and answer the widget's questions.\n3. Let the widget add the booking to the Shopify cart, then send the customer through Shopify Checkout.\n4. After checkout, the app processes the Shopify order and creates or confirms the booking. The app sends its booking confirmation according to the merchant's settings.\n\nSee [How the app works](https://www.getservicify.com/help/getting-started/how-the-app-works). A custom storefront that requires payment must hand off to that product-page flow. Do not first create a public API reservation for the same purchase. Creating a reservation, making an independent cart, and later patching `orderId` is not a supported checkout sequence and can create duplicate reservations.\n\nThe `linkOrder` update example only records a reference to an order that already exists. It does not charge the customer, verify payment, or coordinate checkout, holds, or order webhooks. The `temporary` booking type and `expiresAt` describe records made by the app's own checkout flow; v1 cannot create or extend those holds.\n\n## Gated events and booking status\n\nEvents with `shouldGateBooking: true` are unsupported for customer booking through public API v1. Use the standard storefront widget for these events. This flag enables the widget's customer eligibility check; it does not mean merchant approval. The public slots and create routes do not run that customer check. They can still return slots and create an active reservation, so a successful API response must not be used as proof that a customer passed the gate.\n\nThere is no `pending` approval status or public approve/reject endpoint. An active booking has `status: null`; `checked_in` records attendance, and `deleted` or a non-null `deletedAt` means cancelled. `GET /bookings/{id}` requires `bookings:read` and returns the current booking state. It cannot be polled for an approval decision. Do not show an approval-pending message based on `shouldGateBooking`.\n\n## Understand the resource model\n\n- **Event**: The service a customer books, such as a consultation or class. It defines duration, capacity, eligible staff, Shopify variants, and booking policies.\n- **Availability**: A staff profile and work schedule. `regularHours` holds the recurring week; `specialHours` replaces that schedule on specific dates. Despite the resource name, an Availability is not a list of open appointment times.\n- **Location**: A physical or virtual place where appointments happen. Assign a Location to an Event or Availability with `locationId`; the API copies its customer-facing value into the resource's `location` field.\n- **Slot**: A time that is bookable now. `GET /events/{eventId}/slots` calculates slots from the event rules, staff schedules, existing bookings, and capacity. Each result represents one eligible staff member at one time.\n- **Booking**: A customer's reservation for an event and slot. It references the event with `eventId`, the stored timeslot with `eventDateId`, and the assigned staff member with `employeeId`. Complete booking responses include the timeslot as `booking.date`.\n- **Waitlist entry**: A customer's requested event and preferred time. Use `GET /waitlist` to list entries and `POST /waitlist/{id}/message` to send an invitation when a time opens.\n\nUse `booking-options` to learn what inputs are valid. Use `slots` to learn what can be booked now.\n\n**Staff ID naming:** Availability is the public staff resource. Booking creation and assignment requests use `availabilityId`, while event payloads use the legacy `employees` array and booking responses use the legacy `employeeId` field. All three contain Availability IDs.\n\n## Request format and version\n\nThe API version is part of the URL. Use the `/v1` base path shown above; no version header is required. Send write bodies as JSON with `Content-Type: application/json`.\n\nEasy Appointment Booking resource IDs, such as booking, event, Availability, and Location IDs, are integers. Shopify product, variant, order, and customer IDs are returned as strings.\n\n## Times and timezones\n\nUse IANA timezone names such as `America/Toronto`. Slot searches interpret `from` and `to` as local calendar dates in the requested timezone, unless the event has a locked timezone.\n\nA slot's `startAt` and `endAt` are exact ISO 8601 UTC timestamps. Send the chosen `startAt` back unchanged when creating a booking. In booking responses, prefer `date.utcStartTimeV2` and `date.utcEndTimeV2` for exact scheduling. `startDate`, `startTime`, `utcStartTime`, and `utcEndTime` are retained for compatibility.\n\nWhen the event does not lock its timezone, the customer's `timezone` determines how the selected time is interpreted and displayed. Send the customer's actual IANA timezone.\n\n## Safely retry writes\n\nEvery write endpoint accepts an `Idempotency-Key` header. `POST /bookings`, `POST /waitlist`, and `POST /waitlist/{id}/message` require one because they can trigger customer side effects. Generate a new unique value for each logical write and reuse it only when retrying that same request. Keys are shared across all credentials for a shop and retained for 24 hours.\n\n- Same key and same method, path, and body: the API returns the stored response and sets `Idempotent-Replay: true`.\n- Same key with a different request: the API returns `409 IDEMPOTENCY_REPLAY_MISMATCH`.\n- A second request while the first is still running: the API returns `409 IDEMPOTENCY_IN_FLIGHT`.\n\nIf a network failure leaves the result uncertain, retry with the same key. Do not create a new key for that retry.\n\n## Pagination and response shapes\n\nList endpoints do not all use the same pagination format in v1. Follow the response schema for the endpoint you are calling.\n\n- `GET /events` uses a cursor. Omit `cursor` on the first request, then pass `nextCursor` to the next request. Stop when `hasMore` is `false`. The default `limit` is 50 and the maximum is 200.\n- `GET /bookings` uses 1-based page numbers. The default `limit` is 30. Results are in `dates.results`, and `dates.total` is the total number of matches.\n- `GET /availability` is not paginated and returns an array.\n- `GET /waitlist` uses 1-based page numbers with a default limit of 30 and a maximum of 100. It returns `data` and a `pagination` object; filter dates use UTC day boundaries.\n\n## Errors\n\nError responses use this shape:\n\n```json\n{\n  \"error\": {\n    \"code\": \"FORBIDDEN_SCOPE\",\n    \"message\": \"Missing required scope: bookings:read\",\n    \"requiredScope\": \"bookings:read\"\n  }\n}\n```\n\nUse `error.code` in program logic. Messages are written for people and may become clearer over time.\n\nA `500` response can carry an `error.errorId`. It identifies the failure in the server logs. The API does not return the underlying cause, so quote this value when you contact support and they can find it.\n\n| Status | Meaning |\n| --- | --- |\n| `400` | The request or a required header is malformed |\n| `401` | The bearer token is missing, invalid, expired, or revoked |\n| `402` | The shop's plan does not allow API access |\n| `403` | The key does not have the endpoint's required scope |\n| `404` | The resource does not exist or belongs to another shop |\n| `409` | The request conflicts with current capacity, eligibility, or idempotency state |\n| `422` | The request is well-formed but contains an invalid value or relationship |\n| `429` | A rate limit was exceeded; wait for the number of seconds in `Retry-After` before retrying |\n| `500` or `503` | The API or a required downstream service could not complete the request |\n\n## Recover from booking errors\n\nBranch on `error.code`, not on the HTTP status alone or the message text. The following codes identify different recovery actions:\n\n| HTTP status | `error.code` | Recovery |\n| --- | --- | --- |\n| 409 | `SLOT_UNAVAILABLE`, `CAPACITY_EXCEEDED` | Fetch fresh slots with the intended quantity and variant. Ask the customer to select an available slot. |\n| 409 | `CUSTOM_FIELD_REQUIRED` | Read booking options again. Collect all required answers, including each attendee's answers, and correct attendee placement. |\n| 409 | `EVENT_INACTIVE` | Stop booking this event. Choose another active event or ask the merchant to activate it. |\n| 409 | `VARIANT_REQUIRED` | Ask the merchant to assign a bookable Shopify variant, then read booking options again. |\n| 409 or 422 | `AVAILABILITY_NOT_ASSIGNED` | Refresh booking options and slots, then choose an assigned staff member. |\n| 409 | `IDEMPOTENCY_IN_FLIGHT` | Wait briefly, then retry the identical request with the same key. Do not submit another booking. |\n| 409 | `IDEMPOTENCY_REPLAY_MISMATCH` | Recover the original request for this key. Retry that request unchanged to obtain its result. Use a new key only for a separate booking attempt after resolving the original result. |\n| 422 | `INVALID_VARIANT`, `INVALID_QUANTITY` | Read booking options again and use a valid variant and quantity. Fetch slots for those values. |\n| 422 | `INVALID_CUSTOM_FIELD` | Use a field ID assigned to this event and the required attendee index. |\n| 422 | `INVALID_TAGS`, `VALIDATION_ERROR` | Correct the fields named in the message. Keep the same intended booking until the input is valid. |\n| 400 | `IDEMPOTENCY_KEY_REQUIRED` | Add a unique `Idempotency-Key` for this booking attempt. |\n| 401 | `UNAUTHENTICATED` | Check the server-side API key. |\n| 402 | `PLAN_UPGRADE_REQUIRED` | Ask the merchant to restore API access for the shop's plan. |\n| 403 | `FORBIDDEN_SCOPE` | Grant the scope in `error.requiredScope`. |\n| 404 | `NOT_FOUND` | Check the resource ID and authenticated shop. |\n| 429 | `RATE_LIMITED` | Wait for `Retry-After` before retrying. |\n| 503 | `CAPACITY_VALIDATION_FAILED` | No booking was created by this attempt. Wait, fetch fresh slots, then make a new attempt. |\n| 400 or 500 | `BOOKING_CREATE_FAILED` | Inspect the message. For a server failure, the write result can be uncertain; retain the key and contact support with `error.errorId` when present. |\n| 500 | `INTERNAL_ERROR` | Retain the key and reconcile the result before making a new booking attempt. Contact support if it continues. |\n\nHandled responses, including errors, can be stored under the idempotency key for 24 hours. After a known rejection and its cause are resolved, use a new key for the corrected booking attempt. Reusing the old key can replay the rejection; changing the body under that key causes a mismatch. If a network or server failure leaves the result uncertain, retry the identical request with the same key first. Never switch keys simply to escape an in-flight, mismatch, or uncertain server result.\n\n## Rate limits\n\nAuthenticated requests are limited to 10 requests per second per shop. Every credential for the same shop shares this limit. Responses include `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset`. If the API returns `429`, wait for the number of seconds in `Retry-After` before retrying.\n\n`GET /reports/peak-hours` also has a separate limit of 30 requests per minute per credential. On that endpoint, the rate-limit headers describe the report-specific window. The shop-wide limit still applies, so always treat `Retry-After` on a `429` response as the source of truth."
  },
  "externalDocs": {
    "description": "Interactive API reference",
    "url": "https://app.getservicify.com/api/public/v1/docs"
  },
  "servers": [
    { "url": "https://app.getservicify.com", "description": "Production" }
  ],
  "security": [{ "bearerAuth": [] }],
  "tags": [
    {
      "name": "Account",
      "description": "Verify an API key before doing other work. `GET /me` identifies the shop, credential, scopes, timezone, API version, and current server time."
    },
    {
      "name": "Events",
      "description": "Events define what customers can book. Use `GET /events` to choose one, `booking-options` to learn which inputs it accepts, and `slots` to find times that can be booked now. Events also have a lifecycle: create one on a Shopify product, read it, update it, and duplicate it onto another product."
    },
    {
      "name": "Availability",
      "description": "Staff profiles and working schedules. Availability records describe when staff can work; they are not live appointment slots. Use an event's `/slots` endpoint to find bookable times."
    },
    {
      "name": "Locations",
      "description": "Physical or virtual places where appointments happen. Create a Location, keep its details current, map it to a Shopify location, and use its ID to assign a customer-facing place to an event or staff Availability."
    },
    {
      "name": "Bookings",
      "description": "Customer reservations. Booking writes use the same live workflows as the app, including capacity checks, calendars, notifications, webhooks, refunds, and waitlist handling."
    },
    {
      "name": "Reports",
      "description": "View aggregated booking activity. Report endpoints allow 30 requests per minute for each credential."
    },
    {
      "name": "Waitlist",
      "description": "Read and manage customer waitlist entries for events with waitlist enabled. Joins and invitations can send customer notifications and merchant webhooks."
    },
    {
      "name": "Meta",
      "description": "Open the machine-readable OpenAPI specification or this interactive reference. Both endpoints are public and do not require a token."
    }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "Token in the form srv_live_..."
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "schema": { "type": "string" },
        "description": "Optional unique value for safely retrying a write request. A successful retry with the same key and request body returns the original response. Reusing the key with a different body returns `409 IDEMPOTENCY_REPLAY_MISMATCH`."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "An API error. Use `error.code` for program logic and `error.message` for display or debugging.",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": { "type": "string" },
              "message": { "type": "string" },
              "requiredScope": { "type": "string", "description": "Scope required by the endpoint. Present on `FORBIDDEN_SCOPE` errors." },
              "details": { "type": "object", "additionalProperties": true, "description": "Additional structured context when the error has details to report." },
              "errorId": { "type": "string", "description": "Identifies this failure in the server logs. Present on some `500` responses. Quote it when you contact support." }
            },
            "required": ["code", "message"]
          }
        },
        "example": {
          "error": {
            "code": "FORBIDDEN_SCOPE",
            "message": "Missing required scope: bookings:read",
            "requiredScope": "bookings:read"
          }
        }
      },
      "PeakHoursBucket": {
        "type": "object",
        "additionalProperties": false,
        "required": ["dayOfWeek", "hourOfDay", "bookingCount"],
        "properties": {
          "dayOfWeek": { "type": "integer", "minimum": 0, "maximum": 6, "description": "Day of week, where 0 is Sunday and 6 is Saturday." },
          "hourOfDay": { "type": "integer", "minimum": 0, "maximum": 23, "description": "Hour beginning in the requested timezone (0 through 23)." },
          "bookingCount": { "type": "integer", "minimum": 0 }
        }
      },
      "PeakHoursMeta": {
        "type": "object",
        "additionalProperties": false,
        "required": ["from", "to", "timezone", "eventId", "missingBucketsAreZero"],
        "properties": {
          "from": { "type": "string", "format": "date" },
          "to": { "type": "string", "format": "date" },
          "timezone": { "type": "string", "description": "IANA timezone used for date boundaries and bucket grouping." },
          "eventId": { "type": ["integer", "null"], "minimum": 1, "description": "Applied event filter, or null when all shop events are included." },
          "missingBucketsAreZero": { "type": "boolean", "const": true, "description": "If a bucket is missing from `data`, its `bookingCount` is zero." }
        }
      },
      "PeakHoursResponse": {
        "type": "object",
        "additionalProperties": false,
        "required": ["data", "meta"],
        "properties": {
          "data": { "type": "array", "description": "Contains only buckets with at least one booking.", "items": { "$ref": "#/components/schemas/PeakHoursBucket" } },
          "meta": { "$ref": "#/components/schemas/PeakHoursMeta" }
        }
      },
      "Event": {
        "type": "object",
        "description": "A service that customers can book, such as a 60-minute consultation. Each event is linked to a Shopify product and variant and contains the scheduling, notification, and pricing rules used by the storefront booking flow.",
        "required": ["id", "userId", "slug", "isActive", "createdAt", "updatedAt"],
        "properties": {
          "id": { "type": "integer" },
          "name": { "type": ["string", "null"], "description": "Display name shown to customers." },
          "type": { "type": ["string", "null"], "description": "Event type identifier used by the storefront UI (e.g. \"appointment\", \"class\", \"course\")." },
          "productId": { "type": ["string", "null"], "description": "Shopify product ID this event is attached to (numeric string)." },
          "userId": { "type": "integer", "description": "Owning shop's internal user ID." },
          "variantId": { "type": ["string", "null"], "description": "Default Shopify variant ID purchased when booking this event." },
          "variants": { "type": ["array", "null"], "items": { "type": "string" }, "description": "Additional Shopify variant IDs available for multi-variant (e.g. multi-duration) events." },
          "slug": { "type": "string", "format": "uuid", "description": "Stable UUID identifier used in public booking URLs." },
          "description": { "type": ["string", "null"], "description": "Long-form description shown on the booking page; may contain HTML." },
          "notes": { "type": ["string", "null"], "description": "Internal merchant-only notes; not shown to customers." },
          "isActive": { "type": "boolean", "description": "When false, the event is hidden from the storefront and rejects new bookings." },
          "duration": { "type": ["integer", "null"], "description": "Length of a single booking in minutes." },
          "resolution": { "type": "integer", "default": 30, "description": "Number of minutes between offered start times." },
          "bufferMinutes": { "type": "integer", "default": 0, "description": "Number of unavailable minutes added before and after each booking." },
          "bookMinutesIntoFuture": { "type": ["integer", "null"], "description": "Maximum minutes into the future a customer can book." },
          "timeslotVisibilityMinutes": { "type": ["string", "null"], "description": "How many minutes in advance timeslots become visible to customers. Returned as a string." },
          "leadTime": {
            "type": ["object", "null"],
            "description": "Minimum lead time before a slot can be booked.",
            "properties": {
              "duration": { "type": "integer" },
              "unit": { "type": "string", "enum": ["minutes", "hours", "days"] }
            },
            "additionalProperties": true
          },
          "startAt": { "type": ["string", "null"], "format": "date-time", "description": "Earliest date the event is available for booking." },
          "endAt": { "type": ["string", "null"], "format": "date-time", "description": "Latest date the event is available for booking." },
          "numAttendees": { "type": ["integer", "null"], "description": "Maximum attendees that can share a single timeslot." },
          "minimumAttendeesPerTimeslot": { "type": "integer", "default": 1, "description": "Minimum attendees required before a timeslot will run." },
          "maximumAttendeesPerTimeslot": { "type": ["integer", "null"], "description": "Maximum attendees allowed in one timeslot. When set, this value overrides `numAttendees`." },
          "dailyMax": { "type": ["integer", "null"], "description": "Maximum number of bookings allowed per calendar day." },
          "employees": { "type": ["array", "null"], "items": { "type": "integer" }, "description": "Internal staff IDs eligible to perform this event." },
          "randomlyAssignEmployee": { "type": "boolean", "default": false, "description": "When `true`, the app automatically assigns an eligible staff member when the booking is created." },
          "useLocationFromAssignedEmployee": { "type": "boolean", "default": false, "description": "When `true`, the booking uses the assigned staff member's location instead of the event's location." },
          "location": { "type": ["string", "null"], "description": "Customer-facing value for the physical or virtual location. When `locationId` is set, this mirrors the assigned Location's value." },
          "locationId": { "type": ["integer", "null"], "description": "ID of the assigned Location. Setting this field with `PATCH /events/{id}` also updates `location` with the Location's customer-facing value." },
          "link": { "type": ["string", "null"], "description": "Static meeting link (e.g. permanent Zoom room) included in confirmations." },
          "isGoogleMeetEnabled": { "type": "boolean", "default": false, "description": "Auto-create a Google Meet link for each booking." },
          "isZoomEnabled": { "type": "boolean", "default": false, "description": "Auto-create a Zoom meeting for each booking." },
          "customerUi": { "type": "string", "enum": ["list", "calendar", "weekly"], "default": "list", "description": "Storefront timeslot picker layout." },
          "calendarColor": { "type": "string", "default": "#3F51B5", "description": "Hex color used to render this event on admin calendars." },
          "enableListViewForOverrideDates": { "type": ["boolean", "null"], "default": true, "description": "Whether one-time override dates appear in the storefront's list view." },
          "lockTimezone": { "type": ["string", "null"], "description": "IANA timezone used for this event instead of the customer's timezone." },
          "defaultPhoneCountryCode": { "type": ["string", "null"], "description": "Default ISO country code preselected in the customer phone input." },
          "redirect_url": { "type": ["string", "null"], "format": "uri", "description": "URL to send the customer to after a successful booking. This is the only field on the event model that uses snake_case." },
          "enableNotifications": { "type": "boolean", "default": true, "description": "Controls all email and SMS notifications for this event." },
          "sendConfirmationNotification": { "type": "boolean", "default": true },
          "sendReminderNotification": { "type": "boolean", "default": false },
          "sendThankyouNotification": { "type": "boolean", "default": false },
          "sendConfirmationText": { "type": ["boolean", "null"], "default": false },
          "sendReminderText": { "type": ["boolean", "null"], "default": false },
          "sendThankyouText": { "type": ["boolean", "null"], "default": false },
          "reminderHours": { "type": ["integer", "null"], "default": 24, "description": "Hours before the booking at which reminders are sent." },
          "hasNotificationsEnabled": { "type": "boolean", "readOnly": true, "description": "Computed field. `true` when at least one notification channel is enabled." },
          "addCustomerNameToCalendarDescription": { "type": ["boolean", "null"], "default": false },
          "addAttendeesToCalendarInvite": { "type": "boolean", "default": true },
          "gCalTitle": { "type": ["string", "null"], "description": "Override template for the title used on Google/Outlook calendar invites." },
          "customerRescheduleCutoffInHours": { "type": "integer", "default": 0, "description": "Hours before the booking after which customers may no longer reschedule." },
          "customerCancelCutoffInHours": { "type": "integer", "default": 0, "description": "Hours before the booking after which customers may no longer cancel." },
          "showRescheduleCancelButton": { "type": "boolean", "default": true },
          "shouldGateBooking": { "type": "boolean", "default": false, "description": "Enables the storefront customer eligibility check, not merchant approval. Public v1 slots and booking creation do not enforce this check. Events with this flag are unsupported for customer API booking; use the standard widget. There is no pending approval status or approval polling endpoint." },
          "canBookWithoutCheckout": { "type": ["boolean", "null"], "default": false, "description": "Allow bookings to be created without going through Shopify checkout (e.g. free events)." },
          "fixedPriceBooking": { "type": "boolean", "default": false, "description": "Use a fixed event-level price instead of variant pricing." },
          "depositSettings": { "type": ["object", "null"], "additionalProperties": true, "description": "Deposit configuration (partial payment up front, remainder later)." },
          "upsell": { "type": ["object", "null"], "additionalProperties": true, "description": "Upsell configuration offered to the customer during booking." },
          "isBundle": { "type": ["boolean", "null"], "default": false, "description": "Whether this event is sold as a bundle of multiple bookings." },
          "bundleSize": { "type": ["integer", "null"], "description": "Number of bookings included in a single bundle purchase." },
          "bundleExpirySettings": { "type": ["object", "null"], "additionalProperties": true },
          "waitlistStatus": { "type": ["string", "null"], "enum": [null, "disabled", "no_timeslots", "enabled"], "default": "disabled", "description": "Controls whether customers can join a waitlist when timeslots are full. `no_timeslots` enables waitlist joining only when the event has no available timeslots." },
          "customFields": { "type": ["array", "null"], "items": { "type": "integer" }, "description": "Custom field IDs attached to this event's booking form." },
          "forceCalendarSlots": { "type": ["object", "null"], "additionalProperties": true },
          "uniqueEventDatePerBooking": { "type": "boolean", "default": false, "description": "When `true`, each booking gets a separate timeslot record instead of sharing one with other bookings." },
          "createdAt": { "type": "string", "format": "date-time" },
          "updatedAt": { "type": "string", "format": "date-time" }
        },
        "example": {
          "id": 48213,
          "name": "60-minute Consultation",
          "type": "appointment",
          "productId": "8472913847291",
          "userId": 1042,
          "variantId": "47281937482910",
          "variants": ["47281937482910", "47281937482911"],
          "slug": "9b1c4f2e-7a3d-4f9e-9c8a-1e2b3c4d5e6f",
          "description": "<p>A focused 60-minute session with one of our senior consultants.</p>",
          "notes": "VIP customers only — coordinate with Maya before confirming.",
          "isActive": true,
          "duration": 60,
          "resolution": 30,
          "bufferMinutes": 10,
          "bookMinutesIntoFuture": 43200,
          "timeslotVisibilityMinutes": "20160",
          "leadTime": { "duration": 24, "unit": "hours" },
          "startAt": "2026-05-01T00:00:00.000Z",
          "endAt": "2026-12-31T23:59:59.000Z",
          "numAttendees": 1,
          "minimumAttendeesPerTimeslot": 1,
          "maximumAttendeesPerTimeslot": 1,
          "dailyMax": 8,
          "employees": [12, 17, 23],
          "randomlyAssignEmployee": false,
          "useLocationFromAssignedEmployee": true,
          "location": "Toronto Office — 350 Bay St, Suite 400",
          "locationId": 91,
          "link": null,
          "isGoogleMeetEnabled": false,
          "isZoomEnabled": true,
          "customerUi": "calendar",
          "calendarColor": "#3F51B5",
          "enableListViewForOverrideDates": true,
          "lockTimezone": "America/Toronto",
          "defaultPhoneCountryCode": "CA",
          "redirect_url": "https://example-shop.myshopify.com/pages/thanks",
          "enableNotifications": true,
          "sendConfirmationNotification": true,
          "sendReminderNotification": true,
          "sendThankyouNotification": false,
          "sendConfirmationText": true,
          "sendReminderText": true,
          "sendThankyouText": false,
          "reminderHours": 24,
          "hasNotificationsEnabled": true,
          "addCustomerNameToCalendarDescription": true,
          "addAttendeesToCalendarInvite": true,
          "gCalTitle": "Consultation — {{customerName}}",
          "customerRescheduleCutoffInHours": 12,
          "customerCancelCutoffInHours": 24,
          "showRescheduleCancelButton": true,
          "shouldGateBooking": false,
          "canBookWithoutCheckout": false,
          "fixedPriceBooking": false,
          "depositSettings": { "type": "percentage", "amount": 25 },
          "upsell": null,
          "isBundle": false,
          "bundleSize": null,
          "bundleExpirySettings": null,
          "waitlistStatus": "enabled",
          "customFields": [301, 305],
          "forceCalendarSlots": null,
          "uniqueEventDatePerBooking": true,
          "createdAt": "2026-02-14T18:22:41.000Z",
          "updatedAt": "2026-05-08T09:11:07.000Z"
        }
      },
      "EventDate": {
        "type": "object",
        "description": "A specific timeslot for an event. Booking responses include the timeslot in the `date` field.",
        "required": ["id", "userId", "eventId", "startDate", "endDate"],
        "properties": {
          "id": { "type": "integer" },
          "userId": { "type": "integer" },
          "eventId": { "type": "integer" },
          "startDate": { "type": "string", "format": "date", "description": "Date the slot starts in its local timezone, formatted as `YYYY-MM-DD`." },
          "endDate": { "type": "string", "format": "date" },
          "startTime": { "type": ["string", "null"], "description": "Start time in the slot's local timezone. This is a free-form value such as `10:00 am`." },
          "endTime": { "type": ["string", "null"] },
          "timezone": { "type": ["string", "null"], "description": "IANA timezone the local startDate/startTime are expressed in." },
          "utcStartTimeV2": { "type": ["string", "null"], "format": "date-time", "description": "Exact start time in UTC. Use this instead of `startDate` and `startTime` for scheduling." },
          "utcEndTimeV2": { "type": ["string", "null"], "format": "date-time", "description": "Exact end time in UTC." },
          "utcStartTime": { "type": ["string", "null"], "format": "date-time", "description": "Legacy UTC start. Use utcStartTimeV2 instead." },
          "utcEndTime": { "type": ["string", "null"], "format": "date-time", "description": "Legacy UTC end. Use utcEndTimeV2 instead." },
          "attendees": { "type": ["integer", "null"], "description": "Total seats available in this slot." },
          "location": { "type": ["string", "null"] },
          "link": { "type": ["string", "null"], "description": "Custom meeting link (overrides Zoom/Google Meet if set)." },
          "gCalEventId": { "type": ["string", "null"] },
          "gCalCalendarId": { "type": ["string", "null"] },
          "gCalMeetLink": { "type": ["string", "null"] },
          "zoomMeetingLink": { "type": ["string", "null"] },
          "outlookCalendarId": { "type": ["string", "null"] },
          "outlookEventId": { "type": ["string", "null"] },
          "notes": { "type": ["string", "null"], "description": "Slot-level admin notes." },
          "remindAt": { "type": ["string", "null"], "format": "date-time" },
          "createdAt": { "type": "string", "format": "date-time" },
          "updatedAt": { "type": ["string", "null"], "format": "date-time" }
        }
      },
      "BookingDetail": {
        "type": "object",
        "description": "The latest synchronized snapshot of the Shopify order and customer address. To include it in a booking response, pass `?includeDetails=true` to `GET /bookings/{id}`.",
        "required": ["id"],
        "properties": {
          "id": { "type": "integer" },
          "userId": { "type": ["integer", "null"] },
          "bookingId": { "type": ["integer", "null"] },
          "orderDetails": { "type": ["object", "null"], "additionalProperties": true, "description": "Latest Shopify order snapshot (line items, discounts, totals, payment status, etc.)." },
          "orderDetailsSyncedAt": { "type": ["string", "null"], "format": "date-time", "description": "Time of the last successful Shopify order-details sync." },
          "addressDetails": { "type": ["object", "null"], "additionalProperties": true, "description": "Customer shipping/billing address from the Shopify order." },
          "createdAt": { "type": "string", "format": "date-time" }
        }
      },
      "Booking": {
        "type": "object",
        "description": "A customer's reservation for an event timeslot. `GET /bookings/{id}` and `PATCH /bookings/{id}` return this complete object. `GET /bookings` returns the smaller, flat `BookingListItem` shape instead. A booking is created through a Shopify order or a manual admin checkout. Cancelling a booking sets `deletedAt` instead of permanently deleting the record. Public POST /bookings returns the saved row without guaranteed date data; follow it with GET /bookings/{id} using bookings:read for the complete timeslot.",
        "required": ["id", "userId", "eventId", "eventDateId", "type", "productId", "variantId", "price", "slug", "createdAt", "updatedAt"],
        "properties": {
          "id": { "type": "integer" },
          "userId": { "type": "integer", "description": "ID of the shop that owns the booking. It always matches the authenticated token's shop." },
          "eventId": { "type": "integer", "description": "ID of the event being booked." },
          "eventDateId": { "type": "integer", "description": "ID of the timeslot assigned to the booking." },
          "employeeId": { "type": ["integer", "null"], "description": "ID of the assigned staff member, or `null` when no one is assigned." },
          "purchasedPackageId": { "type": ["integer", "null"], "description": "ID of the purchased package used for this booking, if any." },
          "type": { "type": "string", "enum": ["standard", "temporary", "bundle"], "description": "Booking type: `standard` for a regular booking, `temporary` for a short checkout hold, or `bundle` for one session in a multi-session bundle." },
          "status": {
            "type": ["string", "null"],
            "enum": [null, "checked_in", "deleted"],
            "description": "Booking status. `null` means active, `checked_in` means attendance was recorded, and `deleted` means cancelled. Cancelled bookings also have a `deletedAt` value. This is not approval state. There is no pending approval status in v1."
          },
          "slug": { "type": "string", "description": "UUID slug used in customer-facing reschedule/cancel links." },
          "miniSlug": { "type": ["string", "null"], "description": "Short unique slug for compact URLs (e.g. SMS)." },
          "bundleSlug": { "type": ["string", "null"], "description": "Groups bookings purchased together as a bundle." },
          "conflictSlug": { "type": ["string", "null"], "description": "Groups bookings that were overbooked into the same slot." },
          "firstName": { "type": ["string", "null"] },
          "lastName": { "type": ["string", "null"] },
          "customerEmail": { "type": ["string", "null"], "format": "email", "description": "Email from the Shopify order (the buyer)." },
          "contactEmail": { "type": ["string", "null"], "format": "email", "description": "Preferred contact email; may differ from customerEmail if the buyer is booking on someone else's behalf." },
          "contactPhone": { "type": ["string", "null"] },
          "timezone": { "type": ["string", "null"], "description": "IANA timezone the customer booked in (e.g. America/Toronto)." },
          "quantity": { "type": ["integer", "null"], "description": "Number of attendees on this booking." },
          "bookingCount": { "type": ["integer", "null"], "description": "Stored attendee count used to calculate capacity. It usually matches `quantity`." },
          "price": { "type": "number", "format": "double", "description": "Pre-tax, net-of-discount unit price in the shop currency. Multiply by quantity for non-fixed-price bookings." },
          "isFixedPriceBooking": { "type": "boolean", "description": "True if this booking was charged a fixed price rather than the variant's current price." },
          "isAdminCheckout": { "type": "boolean", "description": "True if a store admin manually created this booking instead of the customer checking out." },
          "waivedAttendeesCount": { "type": ["integer", "null"], "description": "Number of attendees whose payment is waived. Only set for shops with the Awtomic integration enabled. `null` means no attendees are waived." },
          "productId": { "type": "string", "description": "Shopify product ID (numeric, as a string)." },
          "variantId": { "type": "string", "description": "Shopify product variant ID (numeric, as a string)." },
          "orderId": { "type": ["string", "null"], "description": "Shopify order ID. Null for temporary holds and some admin-created bookings." },
          "orderName": { "type": ["string", "null"], "description": "Human-readable Shopify order name, e.g. \"#1042\"." },
          "lineItemId": { "type": ["string", "null"], "description": "Shopify line item ID, used when issuing partial refunds." },
          "customerId": { "type": ["string", "null"], "description": "Shopify customer ID (numeric, as a string)." },
          "draftOrderId": { "type": ["string", "null"], "description": "Shopify draft order ID for bookings awaiting payment." },
          "draftOrderStatus": { "type": ["string", "null"], "description": "Current status of the associated draft order." },
          "depositPaid": { "type": ["number", "null"], "format": "float", "description": "Amount of deposit collected so far." },
          "remainingDepositToCollect": { "type": ["number", "null"], "format": "float", "description": "Outstanding deposit amount still owed." },
          "depositOrders": { "type": ["object", "null"], "additionalProperties": true, "description": "Map of Shopify orders that recorded deposit payments for this booking." },
          "orderProperties": { "type": ["object", "null"], "additionalProperties": true, "description": "Shopify line-item properties captured at checkout, including answers to custom-field questions." },
          "variant": { "type": ["object", "null"], "additionalProperties": true, "description": "Cached snapshot of the Shopify variant (id, title) at booking time." },
          "internalNotes": { "type": ["string", "null"], "description": "Staff-only notes; never shown to customers." },
          "noShowAt": { "type": ["string", "null"], "format": "date-time", "description": "Timestamp the booking was flagged as a no-show; null if attended or pending." },
          "remindedAt": { "type": ["string", "null"], "format": "date-time" },
          "thankedAt": { "type": ["string", "null"], "format": "date-time" },
          "expiresAt": { "type": ["string", "null"], "format": "date-time", "description": "For type=temporary holds, when the hold auto-releases." },
          "deletedAt": { "type": ["string", "null"], "format": "date-time", "description": "Soft-delete timestamp; null means active." },
          "numReschedules": { "type": ["integer", "null"], "description": "How many times this booking has been rescheduled." },
          "createdAt": { "type": "string", "format": "date-time" },
          "updatedAt": { "type": "string", "format": "date-time" },
          "date": {
            "allOf": [{ "$ref": "#/components/schemas/EventDate" }],
            "description": "The saved timeslot. Always included by GET /bookings/{id} (requires bookings:read). POST /bookings does not guarantee this property; follow creation with GET before calendar confirmation."
          },
          "details": {
            "allOf": [{ "$ref": "#/components/schemas/BookingDetail" }],
            "description": "Shopify order and address details. Included only when `GET /bookings/{id}` receives `?includeDetails=true`."
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": { "type": "integer" },
                "label": { "type": "string" }
              }
            },
            "description": "Custom tags assigned to this booking."
          }
        },
        "example": {
          "id": 814203,
          "userId": 1042,
          "eventId": 5117,
          "eventDateId": 88421,
          "employeeId": 312,
          "purchasedPackageId": null,
          "type": "standard",
          "status": null,
          "slug": "9b3f2a4c-7e21-4a8b-9c1d-2f5e6a8b0c11",
          "miniSlug": "k2p9xq",
          "bundleSlug": null,
          "conflictSlug": null,
          "firstName": "Priya",
          "lastName": "Sharma",
          "customerEmail": "priya.sharma@example.com",
          "contactEmail": "priya.sharma@example.com",
          "contactPhone": "+14165551234",
          "timezone": "America/Toronto",
          "quantity": 2,
          "bookingCount": 2,
          "price": 120.00,
          "isFixedPriceBooking": false,
          "isAdminCheckout": false,
          "productId": "7825412934182",
          "variantId": "43219874512903",
          "orderId": "5481923847216",
          "orderName": "#1042",
          "lineItemId": "13298471203948",
          "customerId": "6712398471029",
          "draftOrderId": null,
          "draftOrderStatus": null,
          "depositPaid": null,
          "remainingDepositToCollect": null,
          "depositOrders": null,
          "orderProperties": {
            "Notes for stylist": "Allergic to lavender products",
            "Preferred pronouns": "she/her"
          },
          "variant": { "id": "43219874512903", "title": "60 min / Senior Stylist", "fullTitle": "60 min / Senior Stylist" },
          "internalNotes": "Returning client — usually prefers chair 3.",
          "noShowAt": null,
          "remindedAt": "2026-05-10T14:00:00.000Z",
          "thankedAt": null,
          "expiresAt": null,
          "deletedAt": null,
          "numReschedules": 1,
          "createdAt": "2026-05-02T18:23:11.482Z",
          "updatedAt": "2026-05-10T14:00:00.000Z",
          "date": {
            "id": 88421,
            "userId": 1042,
            "eventId": 5117,
            "startDate": "2026-05-12",
            "endDate": "2026-05-12",
            "startTime": "10:00 am",
            "endTime": "11:00 am",
            "timezone": "America/Toronto",
            "utcStartTimeV2": "2026-05-12T14:00:00.000Z",
            "utcEndTimeV2": "2026-05-12T15:00:00.000Z",
            "attendees": 4,
            "location": "123 Queen St W, Toronto",
            "gCalEventId": "a1b2c3d4e5f6",
            "gCalCalendarId": "primary",
            "zoomMeetingLink": null,
            "outlookEventId": null,
            "createdAt": "2026-04-01T09:00:00.000Z",
            "updatedAt": "2026-04-01T09:00:00.000Z"
          }
        }
      },
      "BookingListItem": {
        "type": "object",
        "description": "The compact, flat booking shape returned by `GET /bookings`. It combines selected fields from the booking, event, and timeslot. Use `GET /bookings/{id}` when you need the complete `Booking` object.",
        "properties": {
          "id": { "type": "integer", "description": "Booking ID." },
          "eventId": { "type": "integer" },
          "eventDateId": { "type": "integer" },
          "eventName": { "type": ["string", "null"], "description": "Pulled from Events.name." },
          "employeeId": { "type": ["integer", "null"] },
          "purchasedPackageId": { "type": ["integer", "null"] },
          "firstName": { "type": ["string", "null"] },
          "lastName": { "type": ["string", "null"] },
          "customerEmail": { "type": ["string", "null"], "format": "email" },
          "contactPhone": { "type": ["string", "null"] },
          "quantity": { "type": ["integer", "null"] },
          "bookingCount": { "type": ["integer", "null"] },
          "price": { "type": "number", "format": "double" },
          "status": { "type": ["string", "null"], "enum": [null, "checked_in", "deleted"] },
          "noShowAt": { "type": ["string", "null"], "format": "date-time" },
          "isAdminCheckout": { "type": "boolean" },
          "isFixedPriceBooking": { "type": "boolean" },
          "orderId": { "type": ["string", "null"] },
          "orderName": { "type": ["string", "null"] },
          "customerId": { "type": ["string", "null"] },
          "variant": { "type": ["object", "null"], "additionalProperties": true },
          "orderProperties": { "type": ["object", "null"], "additionalProperties": true },
          "draftOrderId": { "type": ["string", "null"] },
          "draftOrderStatus": { "type": ["string", "null"] },
          "depositPaid": { "type": ["number", "null"] },
          "remainingDepositToCollect": { "type": ["number", "null"] },
          "depositOrders": { "type": ["object", "null"], "additionalProperties": true },
          "bundleSlug": { "type": ["string", "null"] },
          "conflictSlug": { "type": ["string", "null"] },
          "startDate": { "type": "string", "format": "date" },
          "endDate": { "type": "string", "format": "date" },
          "startTime": { "type": ["string", "null"] },
          "endTime": { "type": ["string", "null"] },
          "utcStartTime": { "type": ["string", "null"], "format": "date-time", "description": "Legacy. Prefer utcStartTimeV2." },
          "utcEndTime": { "type": ["string", "null"], "format": "date-time", "description": "Legacy. Prefer utcEndTimeV2." },
          "utcStartTimeV2": { "type": ["string", "null"], "format": "date-time" },
          "utcEndTimeV2": { "type": ["string", "null"], "format": "date-time" },
          "timezone": { "type": ["string", "null"], "description": "EventDate timezone (the slot's local zone)." },
          "customerTimezone": { "type": ["string", "null"], "description": "Booking timezone (the customer's locale at booking time)." },
          "lockTimezone": { "type": ["string", "null"], "description": "Event-level forced timezone, if any." },
          "gCalEventId": { "type": ["string", "null"] },
          "gCalCalendarId": { "type": ["string", "null"] },
          "gCalMeetLink": { "type": ["string", "null"] },
          "zoomMeetingLink": { "type": ["string", "null"] },
          "link": { "type": ["string", "null"] },
          "deletedAt": { "type": ["string", "null"], "format": "date-time" },
          "createdAt": { "type": "string", "format": "date-time" },
          "updatedAt": { "type": "string", "format": "date-time" }
        }
      },
      "TimeRange": {
        "type": "string",
        "pattern": "^(?:[01]?\\d|2[0-3]):[0-5]\\d-(?:[01]?\\d|2[0-3]):[0-5]\\d$",
        "description": "A local-time range in 24-hour `H:mm-H:mm` format. Ranges cannot have the same start and end time or overlap another range on the same day. Overnight ranges are supported.",
        "examples": ["09:00-12:00"]
      },
      "DailyHours": {
        "type": "array",
        "items": { "$ref": "#/components/schemas/TimeRange" },
        "description": "One or more working-time ranges. An empty array means the staff member is unavailable all day."
      },
      "WeeklyHours": {
        "type": "object",
        "description": "The complete recurring weekly schedule. Use the exact English day names `Monday` through `Sunday` as keys.",
        "additionalProperties": false,
        "properties": {
          "Monday": { "$ref": "#/components/schemas/DailyHours" },
          "Tuesday": { "$ref": "#/components/schemas/DailyHours" },
          "Wednesday": { "$ref": "#/components/schemas/DailyHours" },
          "Thursday": { "$ref": "#/components/schemas/DailyHours" },
          "Friday": { "$ref": "#/components/schemas/DailyHours" },
          "Saturday": { "$ref": "#/components/schemas/DailyHours" },
          "Sunday": { "$ref": "#/components/schemas/DailyHours" }
        }
      },
      "SpecialHours": {
        "type": "object",
        "description": "The complete set of date-specific schedule overrides, keyed by valid `YYYY-MM-DD` dates. An empty array marks that date as unavailable all day.",
        "patternProperties": {
          "^\\d{4}-\\d{2}-\\d{2}$": { "$ref": "#/components/schemas/DailyHours" }
        },
        "additionalProperties": false
      },
      "Availability": {
        "type": "object",
        "description": "A staff member's profile and working schedule. Responses never include passwords, password-reset fields, OAuth credentials, `linkedEmployees`, team-login permissions, or other authentication data.",
        "additionalProperties": false,
        "required": ["id", "userId", "firstName", "lastName", "email", "timezone", "regularHours", "specialHours", "createdAt", "updatedAt"],
        "properties": {
          "id": { "type": "integer" },
          "userId": { "type": "integer", "description": "ID of the shop that owns this availability." },
          "firstName": { "type": "string" },
          "lastName": { "type": "string" },
          "email": { "type": "string", "format": "email", "description": "Staff contact email." },
          "phone": { "type": ["string", "null"], "description": "Optional contact phone number, free-form." },
          "timezone": { "type": "string", "description": "IANA timezone the schedule is interpreted in." },
          "regularHours": { "$ref": "#/components/schemas/WeeklyHours" },
          "specialHours": { "$ref": "#/components/schemas/SpecialHours" },
          "canConductMultipleEventsSimultaneously": { "type": "boolean", "description": "When `true`, this staff member can be assigned to overlapping events." },
          "googleCalendarId": { "type": ["string", "null"], "description": "Google Calendar ID this staff member's bookings sync to. OAuth tokens are not exposed." },
          "outlookCalendarId": { "type": ["string", "null"], "description": "Microsoft Outlook calendar ID this staff member's bookings sync to. OAuth tokens are not exposed." },
          "contactLink": { "type": ["string", "null"], "description": "Optional public-facing contact URL." },
          "isTeamLoginEnabled": { "type": ["boolean", "null"], "description": "Whether team-member sign-in is enabled. The API does not expose credentials or permissions." },
          "location": { "type": ["string", "null"], "description": "Default location label for this staff member's bookings." },
          "locationId": { "type": ["integer", "null"], "description": "ID of the assigned shop location." },
          "isArchived": { "type": ["boolean", "null"], "description": "When `true`, this staff member is hidden from new-booking flows but remains in the system." },
          "description": { "type": ["string", "null"], "description": "Free-form bio shown on storefront / booking surfaces." },
          "imageUrl": { "type": ["string", "null"], "description": "Avatar image URL." },
          "numberOfBookings": {
            "type": "string",
            "description": "Total bookings ever assigned to this staff member. Returned as a decimal string and cached for one hour. Present in `GET /availability` responses but not in `PUT /availability/{id}` responses."
          },
          "createdAt": { "type": "string", "format": "date-time" },
          "updatedAt": { "type": "string", "format": "date-time" }
        },
        "example": {
          "id": 4821,
          "userId": 117,
          "firstName": "Ada",
          "lastName": "Lovelace",
          "email": "ada@example-salon.com",
          "phone": "+1-415-555-0142",
          "timezone": "America/Los_Angeles",
          "regularHours": {
            "Monday": ["09:00-12:00", "13:00-17:00"],
            "Tuesday": ["09:00-17:00"],
            "Wednesday": ["09:00-17:00"],
            "Thursday": ["09:00-17:00"],
            "Friday": ["09:00-15:00"],
            "Saturday": [],
            "Sunday": []
          },
          "specialHours": {
            "2026-05-25": [],
            "2026-12-24": ["09:00-13:00"],
            "2026-12-25": []
          },
          "canConductMultipleEventsSimultaneously": false,
          "googleCalendarId": "ada@example-salon.com",
          "outlookCalendarId": null,
          "contactLink": "https://example-salon.com/team/ada",
          "isTeamLoginEnabled": true,
          "location": "San Francisco - Mission",
          "locationId": 91,
          "isArchived": false,
          "description": "Senior stylist, 8 years experience. Specializes in color.",
          "imageUrl": "https://cdn.example-salon.com/staff/ada.jpg",
          "numberOfBookings": "342",
          "createdAt": "2024-01-15T18:22:04.000Z",
          "updatedAt": "2026-05-08T14:11:39.000Z"
        }
      },
      "Location": {
        "type": "object",
        "description": "A physical or virtual place where appointments happen. Assign its `id` to an Event or Availability with `locationId`. The API then copies the Location's address, or its name when no address is set, into that resource's `location` field.",
        "additionalProperties": false,
        "required": ["id", "userId", "name", "source", "isArchived", "createdAt", "updatedAt"],
        "properties": {
          "id": { "type": "integer" },
          "userId": { "type": "integer", "description": "Owning shop's internal user ID." },
          "name": { "type": "string", "description": "Display label, e.g. \"Downtown Studio\"." },
          "address": { "type": ["string", "null"], "description": "Free-form address shown to customers. Preferred over name when both are set." },
          "notes": { "type": ["string", "null"], "description": "Internal merchant-only notes; not shown to customers." },
          "source": { "type": "string", "enum": ["manual", "shopify"], "description": "\"manual\" for merchant-entered locations, \"shopify\" for locations synced from Shopify." },
          "shopifyLocationId": { "type": ["string", "null"], "description": "Linked Shopify location ID (numeric string), set for synced locations or a POS register link." },
          "isArchived": { "type": "boolean", "description": "When true, this location is hidden from new assignment pickers but existing assignments keep working." },
          "createdAt": { "type": "string", "format": "date-time" },
          "updatedAt": { "type": "string", "format": "date-time" }
        },
        "example": {
          "id": 91,
          "userId": 1042,
          "name": "San Francisco - Mission",
          "address": "3145 18th St, San Francisco, CA 94110",
          "notes": "Street parking only; validate at the front desk.",
          "source": "manual",
          "shopifyLocationId": null,
          "isArchived": false,
          "createdAt": "2024-01-15T18:22:04.000Z",
          "updatedAt": "2026-05-08T14:11:39.000Z"
        }
      },
      "LocationListResponse": {
        "type": "array",
        "items": { "$ref": "#/components/schemas/Location" }
      },
      "LocationCreate": {
        "type": "object",
        "description": "Creates a Location for the authenticated shop. `name` is the only required field. Send `shopifyLocationId` to map the new Location to a Shopify location in the same request, so a store opening needs no trip to the admin.",
        "additionalProperties": false,
        "required": ["name"],
        "properties": {
          "name": { "type": "string", "minLength": 1, "maxLength": 255, "description": "Display label, e.g. \"Downtown Studio\"." },
          "address": { "type": ["string", "null"], "maxLength": 255, "description": "Free-form address shown to customers. Preferred over name when both are set." },
          "notes": { "type": ["string", "null"], "maxLength": 50000, "description": "Internal merchant-only notes; not shown to customers." },
          "shopifyLocationId": { "type": ["string", "integer", "null"], "description": "Shopify location this place maps to. Send a numeric ID or a `gid://shopify/Location/...` global ID; the API stores the numeric form. A Shopify location can be mapped to one Location per shop, so mapping one that another Location already holds moves it; two requests racing for the same one answer 409 `LOCATION_CONFLICT` for the loser. Send `null` to unmap." }
        },
        "example": {
          "name": "San Francisco - Mission",
          "address": "3145 18th St, San Francisco, CA 94110",
          "notes": "Street parking only; validate at the front desk.",
          "shopifyLocationId": "71234567890"
        }
      },
      "LocationUpdate": {
        "type": "object",
        "description": "Fields `PATCH /locations/{id}` can change. Send only the fields you want changed; omitted fields keep their stored value. Changing `name` or `address` also updates the place shown on the events and staff profiles assigned to this Location.",
        "additionalProperties": false,
        "minProperties": 1,
        "properties": {
          "name": { "type": "string", "minLength": 1, "maxLength": 255 },
          "address": { "type": ["string", "null"], "maxLength": 255 },
          "notes": { "type": ["string", "null"], "maxLength": 50000 },
          "shopifyLocationId": { "type": ["string", "integer", "null"], "description": "Shopify location this place maps to. Send a numeric ID or a `gid://shopify/Location/...` global ID; the API stores the numeric form. A Shopify location can be mapped to one Location per shop, so mapping one that another Location already holds moves it; two requests racing for the same one answer 409 `LOCATION_CONFLICT` for the loser. Send `null` to unmap." }
        },
        "example": { "address": "3145 18th St, Suite 2, San Francisco, CA 94110", "shopifyLocationId": null }
      },
      "BookingUpdate": {
        "type": "object",
        "description": "Fields accepted by `PATCH /bookings/{id}`. The API ignores any field not listed here.",
        "additionalProperties": false,
        "properties": {
          "firstName": { "type": ["string", "null"] },
          "lastName": { "type": ["string", "null"] },
          "contactEmail": { "type": ["string", "null"], "format": "email" },
          "customerEmail": { "type": ["string", "null"], "format": "email" },
          "contactPhone": { "type": ["string", "null"] },
          "internalNotes": { "type": ["string", "null"] },
          "status": {
            "type": ["string", "null"],
            "enum": [null, "checked_in"],
            "description": "Set to `null` for an active booking or `checked_in` to record attendance and update the Shopify order tags. To cancel a booking, use `POST /bookings/{id}/cancel`."
          },
          "orderId": { "type": ["string", "null"], "description": "Shopify order ID. Set to associate the booking with a Shopify order, or null to detach." },
          "orderName": { "type": ["string", "null"], "description": "Human-readable Shopify order name, e.g. \"#1042\"." },
          "customerId": { "type": ["string", "null"], "description": "Shopify customer ID (numeric, as a string)." },
          "price": { "type": "number", "minimum": 0, "description": "Overrides the stored booking price. If the same request changes `orderId` or `orderName`, the app synchronizes the price from that Shopify order and ignores this value. Numeric strings are accepted." },
          "tags": {
            "type": "array",
            "items": { "type": "string" },
            "maxItems": 50,
            "description": "Replaces the booking's tags with exactly this list of labels. Labels are trimmed and de-duplicated; an empty array clears all tags."
          },
          "waivedAttendeesCount": { "type": ["integer", "null"], "minimum": 0, "description": "Number of attendees whose payment is waived, from 0 to the booking's `quantity`. Set `null` to clear the waiver. Ignored unless the shop has the Awtomic integration enabled. A change sends the `booking.updated` webhook." }
        },
        "example": {
          "firstName": "Priya",
          "lastName": "Sharma",
          "contactEmail": "priya.sharma@example.com",
          "contactPhone": "+14165551234",
          "internalNotes": "Confirmed by phone, please reserve chair 3.",
          "status": "checked_in",
          "orderId": "5481923847216",
          "orderName": "#1042",
          "customerId": "6712398471029",
          "price": 75,
          "tags": ["VIP", "Repeat customer"]
        }
      },
      "AvailabilityCreate": {
        "type": "object",
        "description": "Creates a staff profile and schedule for the authenticated shop. The server sets ownership and system fields.",
        "additionalProperties": false,
        "required": ["firstName", "lastName", "email", "timezone"],
        "properties": {
          "firstName": { "type": "string", "minLength": 1, "maxLength": 255 },
          "lastName": { "type": "string", "minLength": 1, "maxLength": 255 },
          "email": { "type": "string", "format": "email", "maxLength": 255 },
          "phone": { "type": ["string", "null"], "maxLength": 255 },
          "timezone": { "type": "string", "description": "Valid IANA timezone name." },
          "regularHours": { "$ref": "#/components/schemas/WeeklyHours" },
          "specialHours": { "$ref": "#/components/schemas/SpecialHours" },
          "canConductMultipleEventsSimultaneously": { "type": "boolean" },
          "contactLink": { "type": ["string", "null"], "format": "uri", "maxLength": 2048 },
          "location": { "type": ["string", "null"], "maxLength": 255 },
          "locationId": { "type": ["integer", "null"], "minimum": 1, "description": "Assign a Location owned by this shop (see GET /locations). Overwrites location with the Location's customer-facing value." },
          "description": { "type": ["string", "null"], "maxLength": 50000 },
          "imageUrl": { "type": ["string", "null"], "format": "uri", "maxLength": 2048 }
        },
        "example": {
          "firstName": "Ada",
          "lastName": "Lovelace",
          "email": "ada@example-salon.com",
          "timezone": "America/Los_Angeles",
          "locationId": 91,
          "regularHours": {
            "Monday": ["09:00-12:00", "13:00-17:00"],
            "Tuesday": ["09:00-17:00"],
            "Wednesday": ["09:00-17:00"],
            "Thursday": ["09:00-17:00"],
            "Friday": ["09:00-15:00"],
            "Saturday": [],
            "Sunday": []
          },
          "specialHours": {},
          "canConductMultipleEventsSimultaneously": false
        }
      },
      "BookingAvailabilityUpdate": {
        "type": "object",
        "description": "Assigns a booking to an active staff availability from the same shop. The staff member must be eligible for the event and available on the booking date.",
        "additionalProperties": false,
        "required": ["availabilityId"],
        "properties": {
          "availabilityId": { "type": "integer", "minimum": 1 }
        },
        "example": { "availabilityId": 4821 }
      },
      "BookingReschedule": {
        "type": "object",
        "description": "Moves a booking to another time for the same event. Provide either an existing `eventDateId` or all required local date and time fields. You cannot move a booking to a different event or change its quantity with this request.",
        "additionalProperties": false,
        "properties": {
          "eventDateId": { "type": "integer", "minimum": 1, "description": "An existing timeslot belonging to the booking's event and shop." },
          "startDate": { "type": "string", "format": "date", "description": "Local start date in the supplied timezone." },
          "endDate": { "type": "string", "format": "date", "description": "Local end date. Defaults to startDate when omitted." },
          "startTime": { "type": "string", "pattern": "^(?:[01]?\\d|2[0-3]):[0-5]\\d$", "description": "Local 24-hour start time in H:mm form." },
          "endTime": { "type": "string", "pattern": "^(?:[01]?\\d|2[0-3]):[0-5]\\d$", "description": "Local 24-hour end time in H:mm form." },
          "timezone": { "type": "string", "description": "Valid IANA timezone name for the local date/time fields." },
          "availabilityId": { "type": "integer", "minimum": 1, "description": "ID of an active, eligible staff availability from the same shop. Required when the booking has no staff assignment. Otherwise, omit it to keep the current assignment." },
          "sendRescheduleEmail": { "type": "boolean", "default": false, "description": "When true, sends the configured reschedule notification after the booking is moved." },
          "rescheduleReason": { "type": ["string", "null"], "maxLength": 2000, "description": "Optional merchant-entered reason stored with the reschedule." },
          "waivedAttendeesCount": { "type": ["integer", "null"], "minimum": 0, "description": "Number of attendees whose payment is waived, from 0 to the booking's `quantity`. Omit it to keep the stored value; set `null` to clear the waiver. Ignored unless the shop has the Awtomic integration enabled." }
        },
        "oneOf": [
          {
            "title": "Use an existing timeslot",
            "required": ["eventDateId"],
            "not": { "anyOf": [{ "required": ["startDate"] }, { "required": ["startTime"] }, { "required": ["endTime"] }, { "required": ["timezone"] }, { "required": ["endDate"] }] }
          },
          {
            "title": "Create or find a timeslot from local time",
            "required": ["startDate", "startTime", "endTime", "timezone"],
            "not": { "required": ["eventDateId"] }
          }
        ],
        "example": {
          "startDate": "2026-08-12",
          "startTime": "10:00",
          "endTime": "11:00",
          "timezone": "America/Toronto",
          "availabilityId": 4821,
          "sendRescheduleEmail": true,
          "rescheduleReason": "Customer requested a later appointment"
        }
      },
      "BookingCancel": {
        "type": "object",
        "description": "Controls customer notification and refund behavior when cancelling. The booking remains available in the shop's history after cancellation.",
        "additionalProperties": false,
        "properties": {
          "sendCancellationEmail": { "type": "boolean", "default": false, "description": "When true, sends the configured cancellation notification to the customer." },
          "sendRefund": { "type": "boolean", "default": false, "description": "When `true`, starts the app's refund workflow. The response reports eligibility and provider errors without exposing credentials." },
          "note": { "type": ["string", "null"], "maxLength": 2000, "description": "Optional merchant cancellation note." }
        },
        "example": {
          "sendCancellationEmail": true,
          "sendRefund": false,
          "note": "Customer requested cancellation"
        }
      },
      "BookingRescheduleResponse": {
        "type": "object",
        "additionalProperties": false,
        "required": ["booking", "didDeleteEventDate"],
        "properties": {
          "booking": { "$ref": "#/components/schemas/Booking" },
          "didDeleteEventDate": { "type": "boolean", "description": "`true` when rescheduling removed the original timeslot because no bookings still use it." }
        }
      },
      "EventCreate": {
        "type": "object",
        "description": "Fields for `POST /events`. `name`, `duration`, `productId`, and `variants` are required; every other field falls back to the app's default.\n\nStaff: omit `employees` and the API creates one availability for the event, with a schedule taken from `scheduleType`. Send `employees` to use availability that already exists. Send an empty array to create the event with no availability; it offers no slots until you assign some.\n\nThis endpoint cannot set ticketing, deposits, subscriptions, capacity rules, or ownership.",
        "additionalProperties": false,
        "required": ["name", "duration", "productId", "variants"],
        "properties": {
          "productId": { "type": "string", "description": "Shopify product ID the event is sold through. Send a numeric ID or a `gid://shopify/Product/...` global ID." },
          "variants": { "type": "array", "minItems": 1, "maxItems": 100, "items": { "type": "string" }, "description": "Shopify variant IDs customers buy to book this event. Only one active event can sell a variant." },
          "customFields": { "type": "array", "items": { "type": "integer", "minimum": 1 }, "description": "IDs of the intake questions to ask, in the order they are asked. Read IDs from `GET /events/{eventId}/booking-options` on an event that already asks them." },
          "scheduleType": { "type": "string", "enum": ["recurring", "fewdays", "oneoff", "custom"], "default": "recurring", "description": "Which starting schedule to give the availability the API creates. Used only when you omit `employees`." },
          "name": { "type": "string", "minLength": 1, "maxLength": 255 },
          "description": { "type": ["string", "null"], "maxLength": 50000 },
          "notes": { "type": ["string", "null"], "maxLength": 50000 },
          "isActive": { "type": "boolean" },
          "duration": { "type": ["integer", "null"], "minimum": 5 },
          "resolution": { "type": "integer", "minimum": 5 },
          "bufferMinutes": { "type": "integer", "minimum": 0 },
          "bookMinutesIntoFuture": { "type": ["integer", "null"], "minimum": 0 },
          "timeslotVisibilityMinutes": { "type": ["string", "null"], "pattern": "^\\d+$" },
          "leadTime": {
            "type": ["object", "null"],
            "additionalProperties": false,
            "required": ["duration", "unit"],
            "properties": {
              "duration": { "type": "integer", "minimum": 0 },
              "unit": { "type": "string", "enum": ["minutes", "hours", "days"] }
            }
          },
          "startAt": { "type": ["string", "null"], "format": "date-time" },
          "endAt": { "type": ["string", "null"], "format": "date-time" },
          "numAttendees": { "type": ["integer", "null"], "minimum": 1 },
          "minimumAttendeesPerTimeslot": { "type": "integer", "minimum": 1 },
          "maximumAttendeesPerTimeslot": { "type": ["integer", "null"], "minimum": 1 },
          "dailyMax": { "type": ["integer", "null"], "minimum": 1 },
          "employees": { "type": "array", "items": { "type": "integer", "minimum": 1 }, "uniqueItems": true, "description": "Staff availability IDs eligible for the event. Every ID must belong to the authenticated shop." },
          "randomlyAssignEmployee": { "type": "boolean" },
          "useLocationFromAssignedEmployee": { "type": "boolean" },
          "location": { "type": ["string", "null"], "maxLength": 255 },
          "locationId": { "type": ["integer", "null"], "minimum": 1, "description": "Assign a Location owned by this shop (see GET /locations), or null to clear it. Overwrites location with the Location's customer-facing value." },
          "link": { "type": ["string", "null"], "format": "uri", "maxLength": 2048, "description": "HTTP(S) booking or meeting URL." },
          "customerUi": { "type": "string", "enum": ["list", "calendar", "weekly"] },
          "calendarColor": { "type": "string", "pattern": "^#[0-9A-Fa-f]{6}$" },
          "enableListViewForOverrideDates": { "type": "boolean" },
          "lockTimezone": { "type": ["string", "null"], "description": "Valid IANA timezone name, or null to use the customer's timezone." },
          "defaultPhoneCountryCode": { "type": ["string", "null"], "maxLength": 32 },
          "enableNotifications": { "type": "boolean" },
          "sendConfirmationNotification": { "type": "boolean" },
          "sendReminderNotification": { "type": "boolean" },
          "sendThankyouNotification": { "type": "boolean" },
          "sendConfirmationText": { "type": "boolean" },
          "sendReminderText": { "type": "boolean" },
          "sendThankyouText": { "type": "boolean" },
          "reminderHours": { "type": "integer", "minimum": 0 },
          "addAttendeesToCalendarInvite": { "type": "boolean" },
          "addCustomerNameToCalendarDescription": { "type": "boolean" },
          "customerRescheduleCutoffInHours": { "type": "integer", "minimum": 0 },
          "customerCancelCutoffInHours": { "type": "integer", "minimum": 0 },
          "showRescheduleCancelButton": { "type": "boolean" },
          "waitlistStatus": { "type": "string", "enum": ["disabled", "no_timeslots", "enabled"] },
          "redirect_url": { "type": ["string", "null"], "format": "uri", "maxLength": 2048, "description": "HTTP or HTTPS URL to send the customer to after booking. This legacy field is the only request property that uses snake_case." },
          "isGoogleMeetEnabled": { "type": "boolean" },
          "isZoomEnabled": { "type": "boolean" },
          "uniqueEventDatePerBooking": { "type": "boolean" },
          "shouldGateBooking": { "type": "boolean", "description": "Enables the storefront customer eligibility check, not merchant approval. Public v1 slots and booking creation do not enforce this check. Events with this flag are unsupported for customer API booking; use the standard widget. There is no pending approval status or approval polling endpoint." },
          "canBookWithoutCheckout": { "type": "boolean", "description": "Let customers book without going through Shopify checkout, e.g. for a free in-store program. Cannot be combined with ticketing; the API rejects the request when the event has ticketing enabled." }
        },
        "example": {
          "name": "60-minute Consultation",
          "duration": 60,
          "productId": "8412345678901",
          "variants": ["44123456789012"],
          "numAttendees": 1,
          "scheduleType": "recurring",
          "sendConfirmationNotification": true,
          "canBookWithoutCheckout": false
        }
      },
      "EventDuplicate": {
        "type": "object",
        "description": "Fields for `POST /events/{id}/duplicate`. The copy takes every setting from the source event, so it needs only its own name and Shopify product link. Ticketing is never copied.",
        "additionalProperties": false,
        "required": ["name", "productId", "variants"],
        "properties": {
          "name": { "type": "string", "minLength": 1, "maxLength": 255, "description": "Name of the copy." },
          "productId": { "type": "string", "description": "Shopify product ID the copy is sold through." },
          "variants": { "type": "array", "minItems": 1, "maxItems": 100, "items": { "type": "string" }, "description": "Shopify variant IDs for the copy. They cannot be the variants the source event sells." }
        },
        "example": { "name": "60-minute Consultation (Spanish)", "productId": "8498765432109", "variants": ["44198765432109"] }
      },
      "EventUpdate": {
        "type": "object",
        "description": "Event fields that `PATCH /events/{id}` can change. This endpoint cannot change product or variant links, ownership, payment and deposit settings, custom fields, capacity rules, or system fields. The schema accepts no other properties, so send only the fields listed here.",
        "additionalProperties": false,
        "minProperties": 1,
        "properties": {
          "name": { "type": "string", "minLength": 1, "maxLength": 255 },
          "description": { "type": ["string", "null"], "maxLength": 50000 },
          "notes": { "type": ["string", "null"], "maxLength": 50000 },
          "isActive": { "type": "boolean" },
          "duration": { "type": ["integer", "null"], "minimum": 5 },
          "resolution": { "type": "integer", "minimum": 5 },
          "bufferMinutes": { "type": "integer", "minimum": 0 },
          "bookMinutesIntoFuture": { "type": ["integer", "null"], "minimum": 0 },
          "timeslotVisibilityMinutes": { "type": ["string", "null"], "pattern": "^\\d+$" },
          "leadTime": {
            "type": ["object", "null"],
            "additionalProperties": false,
            "required": ["duration", "unit"],
            "properties": {
              "duration": { "type": "integer", "minimum": 0 },
              "unit": { "type": "string", "enum": ["minutes", "hours", "days"] }
            }
          },
          "startAt": { "type": ["string", "null"], "format": "date-time" },
          "endAt": { "type": ["string", "null"], "format": "date-time" },
          "numAttendees": { "type": ["integer", "null"], "minimum": 1 },
          "minimumAttendeesPerTimeslot": { "type": "integer", "minimum": 1 },
          "maximumAttendeesPerTimeslot": { "type": ["integer", "null"], "minimum": 1 },
          "dailyMax": { "type": ["integer", "null"], "minimum": 1 },
          "employees": { "type": "array", "items": { "type": "integer", "minimum": 1 }, "uniqueItems": true, "description": "Staff availability IDs eligible for the event. Every ID must belong to the authenticated shop." },
          "randomlyAssignEmployee": { "type": "boolean" },
          "useLocationFromAssignedEmployee": { "type": "boolean" },
          "location": { "type": ["string", "null"], "maxLength": 255 },
          "locationId": { "type": ["integer", "null"], "minimum": 1, "description": "Assign a Location owned by this shop (see GET /locations), or null to clear it. Overwrites location with the Location's customer-facing value." },
          "link": { "type": ["string", "null"], "format": "uri", "maxLength": 2048, "description": "HTTP(S) booking or meeting URL." },
          "customerUi": { "type": "string", "enum": ["list", "calendar", "weekly"] },
          "calendarColor": { "type": "string", "pattern": "^#[0-9A-Fa-f]{6}$" },
          "enableListViewForOverrideDates": { "type": "boolean" },
          "lockTimezone": { "type": ["string", "null"], "description": "Valid IANA timezone name, or null to use the customer's timezone." },
          "defaultPhoneCountryCode": { "type": ["string", "null"], "maxLength": 32 },
          "enableNotifications": { "type": "boolean" },
          "sendConfirmationNotification": { "type": "boolean" },
          "sendReminderNotification": { "type": "boolean" },
          "sendThankyouNotification": { "type": "boolean" },
          "sendConfirmationText": { "type": "boolean" },
          "sendReminderText": { "type": "boolean" },
          "sendThankyouText": { "type": "boolean" },
          "reminderHours": { "type": "integer", "minimum": 0 },
          "addAttendeesToCalendarInvite": { "type": "boolean" },
          "addCustomerNameToCalendarDescription": { "type": "boolean" },
          "customerRescheduleCutoffInHours": { "type": "integer", "minimum": 0 },
          "customerCancelCutoffInHours": { "type": "integer", "minimum": 0 },
          "showRescheduleCancelButton": { "type": "boolean" },
          "waitlistStatus": { "type": "string", "enum": ["disabled", "no_timeslots", "enabled"] },
          "redirect_url": { "type": ["string", "null"], "format": "uri", "maxLength": 2048, "description": "HTTP or HTTPS URL to send the customer to after booking. This legacy field is the only request property that uses snake_case." },
          "isGoogleMeetEnabled": { "type": "boolean" },
          "isZoomEnabled": { "type": "boolean" },
          "uniqueEventDatePerBooking": { "type": "boolean" },
          "shouldGateBooking": {"type": "boolean", "description": "Enables the storefront customer eligibility check, not merchant approval. Public v1 slots and booking creation do not enforce this check. Events with this flag are unsupported for customer API booking; use the standard widget. There is no pending approval status or approval polling endpoint."},
          "canBookWithoutCheckout": { "type": "boolean", "description": "Let customers book without going through Shopify checkout, e.g. for a free in-store program. Cannot be combined with ticketing; the API rejects the request when the event has ticketing enabled." }
        },
        "example": {
          "name": "75-minute Consultation",
          "duration": 75,
          "resolution": 15,
          "bufferMinutes": 10,
          "employees": [12, 17],
          "leadTime": { "duration": 24, "unit": "hours" },
          "sendReminderNotification": true,
          "reminderHours": 24
        }
      },
      "AvailabilityUpdate": {
        "type": "object",
        "description": "Schedule and lifecycle fields accepted by `PUT /availability/{id}`. The API ignores other fields, including name, email, phone, calendar IDs, password, and `linkedEmployees`.",
        "additionalProperties": false,
        "minProperties": 1,
        "properties": {
          "regularHours": { "$ref": "#/components/schemas/WeeklyHours" },
          "specialHours": { "$ref": "#/components/schemas/SpecialHours" },
          "locationId": { "type": ["integer", "null"], "minimum": 1, "description": "Assign a Location owned by this shop (see GET /locations), or null to clear it. Overwrites location with the Location's customer-facing value." },
          "isArchived": { "type": "boolean", "description": "Retire this staff profile, or restore it. An archived profile offers no new slots and is hidden from assignment pickers; its bookings and history stay." },
          "isTeamLoginEnabled": { "type": "boolean", "description": "Grant or withdraw Team Portal access. Granting it takes one of the shop's purchased seats; the API answers 422 `NO_SEATS_AVAILABLE` when every seat is taken. A newly granted staff member sets their own password from the Team Portal sign-in page." }
        },
        "example": {
          "regularHours": {
            "Monday": ["09:00-12:00", "13:00-17:00"],
            "Tuesday": ["09:00-17:00"],
            "Wednesday": ["09:00-17:00"],
            "Thursday": ["09:00-17:00"],
            "Friday": ["09:00-15:00"],
            "Saturday": [],
            "Sunday": []
          },
          "specialHours": {
            "2026-12-24": ["09:00-13:00"],
            "2026-12-25": []
          }
        }
      },
      "Capability": {
        "type": "object",
        "description": "Details about the current API token, returned by `GET /me`. Use this response to verify the shop and granted scopes. `serverTime` helps clients account for differences between their clock and the server clock.",
        "required": ["shop", "userId", "credential", "serverTime", "apiVersion"],
        "properties": {
          "shop": { "type": "string", "description": "Shopify shop domain (e.g. example.myshopify.com)." },
          "userId": { "type": "integer", "description": "Internal user/shop ID." },
          "timezone": { "type": ["string", "null"], "description": "Shop's local IANA timezone." },
          "credential": {
            "type": "object",
            "required": ["id", "name", "prefix", "scopes"],
            "properties": {
              "id": { "type": "integer" },
              "name": { "type": "string", "description": "Human-readable credential name set at creation time." },
              "prefix": { "type": "string", "description": "Public prefix of the token (e.g. \"srv_live_a1b2c3\")." },
              "scopes": { "type": "array", "items": { "type": "string" }, "description": "Granted scope strings (e.g. \"bookings:read\", \"availability:write\", \"reports:read\")." }
            }
          },
          "serverTime": { "type": "string", "format": "date-time" },
          "apiVersion": { "type": "string" }
        },
        "example": {
          "shop": "example-salon.myshopify.com",
          "userId": 1042,
          "timezone": "America/Toronto",
          "credential": {
            "id": 17,
            "name": "Operations agent — May 2026",
            "prefix": "srv_live_a1b2c3d4",
            "scopes": ["bookings:read", "bookings:create", "bookings:write", "availability:read", "availability:write", "events:read", "events:write", "reports:read"]
          },
          "serverTime": "2026-05-11T16:25:09.482Z",
          "apiVersion": "v1"
        }
      },
      "EventListResponse": {
        "type": "object",
        "required": ["data", "nextCursor", "hasMore"],
        "properties": {
          "data": { "type": "array", "items": { "$ref": "#/components/schemas/Event" } },
          "nextCursor": { "type": ["integer", "null"], "description": "Pass this value as `?cursor=...` to fetch the next page. It is `null` when there are no more results." },
          "hasMore": { "type": "boolean" }
        }
      },
      "BookingListResponse": {
        "type": "object",
        "required": ["dates", "totalBookings"],
        "properties": {
          "dates": {
            "type": "object",
            "required": ["results", "total"],
            "properties": {
              "results": { "type": "array", "items": { "$ref": "#/components/schemas/BookingListItem" } },
              "total": { "type": "integer", "description": "Total number of bookings that match the query across all pages." }
            }
          },
          "totalBookings": { "type": ["integer", "null"] }
        }
      },
      "AvailabilityListResponse": {
        "type": "array",
        "items": { "$ref": "#/components/schemas/Availability" }
      },
      "BookingOptionAvailability": {
        "type": "object",
        "additionalProperties": false,
        "required": ["id", "firstName", "lastName", "timezone", "regularHours", "specialHours"],
        "properties": {
          "id": { "type": "integer" },
          "firstName": { "type": "string" },
          "lastName": { "type": "string" },
          "timezone": { "type": "string", "description": "IANA timezone." },
          "regularHours": { "$ref": "#/components/schemas/WeeklyHours" },
          "specialHours": { "$ref": "#/components/schemas/SpecialHours" },
          "canConductMultipleEventsSimultaneously": { "type": "boolean" },
          "contactLink": { "type": ["string", "null"], "format": "uri" },
          "location": { "type": ["string", "null"] },
          "locationId": { "type": ["integer", "null"] },
          "description": { "type": ["string", "null"] },
          "imageUrl": { "type": ["string", "null"], "format": "uri" }
        }
      },
      "BookingCustomField": {
        "type": "object",
        "additionalProperties": false,
        "required": ["id", "label", "inputType", "isRequired", "isPerAttendee"],
        "properties": {
          "id": { "type": "integer" },
          "label": { "type": "string" },
          "inputType": { "type": "string", "enum": ["text", "date", "dropdown", "multiple_choice", "checkbox", "signature"], "description": "Supported control type. See Custom fields and answers for properties and string answer formats." },
          "isRequired": { "type": "boolean", "description": "Collect a non-empty answer. For a checkbox, require Yes in your form; the server only checks that the answer is non-empty." },
          "isPerAttendee": { "type": "boolean", "description": "When true, collect a separate answer for each attendee and send attendeeIndex from 1 through quantity. Otherwise omit attendeeIndex." },
          "helpText": { "type": ["string", "null"] },
          "properties": { "type": ["object", "null"], "additionalProperties": true, "description": "Type-specific configuration. text uses textType (default single); dropdown and multiple_choice use options. date, checkbox, and signature have no type-specific properties. Null or absent means no configuration. Ignore unknown keys.", "properties": {"textType": {"type": "string", "enum": ["single", "multi"], "description": "Text control only: one line or a text area. Default single."}, "options": {"type": "array", "items": {"type": "string"}, "description": "Dropdown and multiple-choice controls: option texts in display order. Use the exact texts as answer values."}} }
        }
      },
      "BookingVariant": {
        "type": "object",
        "additionalProperties": false,
        "required": ["id"],
        "properties": {
          "id": { "type": "string" },
          "title": { "type": ["string", "null"] },
          "displayName": { "type": ["string", "null"] },
          "price": { "type": ["string", "null"], "description": "Decimal catalog price in the default shop currency, in major units (75.00 means 75 currency units). May be absent or null. Read currencyCode from the same shop via Shopify Admin GraphQL: query { shop { myshopifyDomain currencyCode } }. Requires a separate Shopify token. See Prices and currency; this is not a market-specific checkout quote." },
          "sku": { "type": ["string", "null"] }
        }
      },
      "BookingOptions": {
        "type": "object",
        "description": "Inputs and constraints needed before searching for slots or creating a booking for an event.",
        "required": ["eventId", "timezone", "variants", "availability", "customFields", "location", "quantityConstraints"],
        "properties": {
          "eventId": { "type": "integer" },
          "timezone": { "type": "string", "description": "The event's locked timezone when configured. Otherwise, the shop or default staff timezone." },
          "variants": { "type": "array", "items": { "$ref": "#/components/schemas/BookingVariant" } },
          "availability": { "type": "array", "items": { "$ref": "#/components/schemas/BookingOptionAvailability" } },
          "customFields": { "type": "array", "items": { "$ref": "#/components/schemas/BookingCustomField" } },
          "location": {
            "type": ["object", "null"],
            "properties": {
              "id": { "type": ["integer", "null"] },
              "name": { "type": ["string", "null"] },
              "address": { "type": ["string", "null"] }
            }
          },
          "quantityConstraints": {
            "type": "object",
            "required": ["minimum", "maximum", "dailyMaximumBookings"],
            "properties": {
              "minimum": { "type": "integer", "minimum": 1, "description": "Smallest quantity accepted for one booking." },
              "maximum": { "type": "integer", "minimum": 1, "description": "Largest quantity accepted for one booking before live slot capacity is considered." },
              "dailyMaximumBookings": { "type": ["integer", "null"], "minimum": 1, "description": "Event-wide daily booking limit, or `null` when no daily limit is configured." }
            }
          }
        }
      },
      "BookableSlot": {
        "type": "object",
        "required": ["startAt", "endAt", "localDate", "startTime", "endTime", "timezone", "availabilityId", "availabilityName", "remainingCapacity", "variantId"],
        "properties": {
          "startAt": { "type": "string", "format": "date-time", "description": "Exact start time in UTC. Send this value to `POST /bookings`." },
          "endAt": { "type": "string", "format": "date-time" },
          "localDate": { "type": "string", "format": "date" },
          "startTime": { "type": "string", "description": "Start time formatted for the response's `timezone`." },
          "endTime": { "type": "string", "description": "End time formatted for the response's `timezone`." },
          "timezone": { "type": "string" },
          "availabilityId": { "type": "integer", "description": "ID of the staff Availability that can accept this booking." },
          "availabilityName": { "type": "string", "description": "Display name of the staff member represented by `availabilityId`." },
          "remainingCapacity": { "type": "integer", "minimum": 1, "description": "Capacity remaining after accounting for current bookings." },
          "variantId": { "type": "string", "description": "Shopify variant validated for this slot. Send it to `POST /bookings`." }
        }
      },
      "BookableSlotsResponse": {
        "type": "object",
        "description": "Live bookable slots for the requested event, date range, variant, quantity, and timezone.",
        "required": ["eventId", "from", "to", "timezone", "quantity", "variantId", "slots"],
        "properties": {
          "eventId": { "type": "integer" },
          "from": { "type": "string", "format": "date" },
          "to": { "type": "string", "format": "date" },
          "timezone": { "type": "string" },
          "quantity": { "type": "integer", "minimum": 1 },
          "variantId": { "type": "string" },
          "slots": { "type": "array", "items": { "$ref": "#/components/schemas/BookableSlot" } }
        }
      },
      "BookingCreate": {
        "type": "object",
        "description": "Creates a live booking from a slot returned by `GET /events/{eventId}/slots`. Reuse the slot values rather than calculating them in the client.",
        "additionalProperties": false,
        "required": ["eventId", "startAt", "availabilityId", "customer"],
        "properties": {
          "eventId": { "type": "integer", "minimum": 1, "description": "ID of the event being booked." },
          "startAt": { "type": "string", "format": "date-time", "description": "Use the `startAt` value from `GET /events/{eventId}/slots`." },
          "availabilityId": { "type": "integer", "minimum": 1, "description": "Use the `availabilityId` value from the selected slot." },
          "variantId": { "type": "string", "description": "Use the `variantId` from the selected slot. If omitted, the API uses the event's primary variant." },
          "quantity": { "type": "integer", "minimum": 1, "default": 1, "description": "Number of attendees. Use the same quantity that you sent when requesting the selected slot." },
          "customer": {
            "type": "object",
            "additionalProperties": false,
            "required": ["firstName", "lastName", "email", "timezone"],
            "properties": {
              "firstName": { "type": "string", "maxLength": 255 },
              "lastName": { "type": "string", "maxLength": 255 },
              "email": { "type": "string", "format": "email", "maxLength": 255 },
              "phone": { "type": ["string", "null"], "maxLength": 255 },
              "timezone": { "type": "string", "description": "Customer's IANA timezone, such as `America/Toronto`. Used unless the event has a locked timezone." }
            }
          },
          "answers": {
            "type": "array",
            "maxItems": 200,
            "description": "Answers to the event's custom booking fields. Get the field IDs and requirements from `GET /events/{eventId}/booking-options`.",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": ["fieldId", "value"],
              "properties": {
                "fieldId": { "type": "integer", "minimum": 1, "description": "Custom field ID from the event's booking options." },
                "attendeeIndex": { "type": "integer", "minimum": 1, "description": "Required for per-attendee fields, from 1 through quantity. Omit for once-per-booking fields." },
                "value": { "type": "string", "description": "text: plain text; date: YYYY-MM-DD; dropdown: exact option text; multiple_choice: option texts joined with a comma (no escaping); checkbox: Yes or No; signature: PNG data URL (data:image/png;base64,...). The server trims surrounding whitespace. See Custom fields and answers for validation requirements and legacy format limits.", "examples": ["Dietary notes", "2026-08-20", "Vegetarian", "Vegetarian,Gluten free", "Yes"] }
              }
            }
          },
          "internalNotes": { "type": ["string", "null"], "maxLength": 50000, "description": "Staff-only notes. Never shown to the customer." },
          "tags": { "type": "array", "maxItems": 50, "items": { "type": "string", "maxLength": 255 }, "description": "App-only booking tags. They do not sync to Shopify order tags." },
          "sendConfirmation": { "type": "boolean", "default": true, "description": "When `false`, skips the confirmation email and SMS. Calendar and integration actions still follow the merchant's settings." }
        },
        "example": {
          "eventId": 5117,
          "startAt": "2026-08-20T14:00:00.000Z",
          "availabilityId": 312,
          "variantId": "43219874512903",
          "quantity": 1,
          "customer": {
            "firstName": "Priya",
            "lastName": "Sharma",
            "email": "priya.sharma@example.com",
            "phone": "+14165551234",
            "timezone": "America/Toronto"
          },
          "answers": [{ "fieldId": 301, "value": "No accessibility requirements" }],
          "internalNotes": "Booked by phone",
          "sendConfirmation": true
        }
      },
      "WaitlistEntry": {
        "type": "object",
        "description": "A customer waiting for an event time. Every entry belongs to the shop represented by the bearer token.",
        "additionalProperties": false,
        "required": ["id", "userId", "eventId", "name", "email", "preferredTime", "timezone", "variantId", "status", "created_at", "updated_at"],
        "properties": {
          "id": { "type": "integer", "minimum": 1, "maximum": 2147483647 },
          "userId": { "type": "integer", "minimum": 1, "maximum": 2147483647, "description": "Owning shop's internal user ID." },
          "eventId": { "type": "integer", "minimum": 1, "maximum": 2147483647, "description": "Event the customer wants to book." },
          "name": { "type": ["string", "null"], "maxLength": 255 },
          "email": { "type": ["string", "null"], "format": "email", "maxLength": 255 },
          "phone": { "type": ["string", "null"], "maxLength": 255, "readOnly": true, "description": "Phone value when one exists on a stored entry. Public waitlist writes do not set this field." },
          "preferredTime": { "type": "string", "format": "date-time", "description": "Preferred time, returned as an ISO 8601 UTC timestamp." },
          "timezone": { "type": "string", "description": "IANA timezone used to display the preferred time." },
          "preferredEmployeeId": { "type": ["integer", "null"], "minimum": 1, "maximum": 2147483647, "description": "Preferred staff Availability ID, when set." },
          "customerNotes": { "type": ["string", "null"], "readOnly": true, "description": "Customer notes when they exist on a stored entry. Public waitlist writes do not set this field." },
          "staffNotes": { "type": ["string", "null"], "maxLength": 255, "description": "Internal staff note. It is writable with `PATCH /waitlist/{id}`." },
          "variantId": { "type": ["string", "null"], "maxLength": 255, "description": "Shopify variant the customer selected." },
          "status": { "type": "string", "enum": ["waiting", "messaged", "booked", "expired"] },
          "shopifyCustomerId": { "type": ["string", "null"], "description": "Shopify customer ID created or found by the join workflow." },
          "linkedBookingId": { "type": ["integer", "null"], "minimum": 1, "maximum": 2147483647, "description": "Booking linked when this entry becomes booked." },
          "created_at": { "type": "string", "format": "date-time" },
          "updated_at": { "type": "string", "format": "date-time" }
        },
        "example": {
          "id": 8012,
          "userId": 1042,
          "eventId": 5117,
          "name": "Priya Sharma",
          "email": "priya.sharma@example.com",
          "phone": null,
          "preferredTime": "2026-08-20T14:00:00.000Z",
          "timezone": "America/Toronto",
          "preferredEmployeeId": 312,
          "customerNotes": null,
          "staffNotes": "Offer the first weekday opening.",
          "variantId": "43219874512903",
          "status": "waiting",
          "shopifyCustomerId": "6712398471029",
          "linkedBookingId": null,
          "created_at": "2026-08-18T12:07:33.000Z",
          "updated_at": "2026-08-18T12:07:33.000Z"
        }
      },
      "WaitlistListResponse": {
        "type": "object",
        "description": "A page of waitlist entries for the authenticated shop.",
        "additionalProperties": false,
        "required": ["data", "pagination"],
        "properties": {
          "data": { "type": "array", "items": { "$ref": "#/components/schemas/WaitlistEntry" } },
          "pagination": {
            "type": "object",
            "additionalProperties": false,
            "required": ["currentPage", "totalPages", "totalCount", "pageLimit", "hasNextPage", "hasPreviousPage"],
            "properties": {
              "currentPage": { "type": "integer", "minimum": 1 },
              "totalPages": { "type": "integer", "minimum": 0 },
              "totalCount": { "type": "integer", "minimum": 0 },
              "pageLimit": { "type": "integer", "minimum": 1, "maximum": 100 },
              "hasNextPage": { "type": "boolean" },
              "hasPreviousPage": { "type": "boolean" }
            }
          }
        },
        "example": {
          "data": [{
            "id": 8012,
            "userId": 1042,
            "eventId": 5117,
            "name": "Priya Sharma",
            "email": "priya.sharma@example.com",
            "phone": null,
            "preferredTime": "2026-08-20T14:00:00.000Z",
            "timezone": "America/Toronto",
            "preferredEmployeeId": 312,
            "customerNotes": null,
            "staffNotes": "Offer the first weekday opening.",
            "variantId": "43219874512903",
            "status": "waiting",
            "shopifyCustomerId": "6712398471029",
            "linkedBookingId": null,
            "created_at": "2026-08-18T12:07:33.000Z",
            "updated_at": "2026-08-18T12:07:33.000Z"
          }],
          "pagination": { "currentPage": 1, "totalPages": 1, "totalCount": 1, "pageLimit": 30, "hasNextPage": false, "hasPreviousPage": false }
        }
      },
      "WaitlistStats": {
        "type": "object",
        "description": "Waitlist counts for the authenticated shop.",
        "additionalProperties": false,
        "required": ["activeCount", "bookedCount", "messagedCount", "expiredCount"],
        "properties": {
          "activeCount": { "type": "integer", "minimum": 0, "description": "Entries with a preferred time in the future, regardless of status." },
          "bookedCount": { "type": "integer", "minimum": 0 },
          "messagedCount": { "type": "integer", "minimum": 0 },
          "expiredCount": { "type": "integer", "minimum": 0 }
        },
        "example": { "activeCount": 14, "bookedCount": 3, "messagedCount": 8, "expiredCount": 5 }
      },
      "WaitlistCreate": {
        "type": "object",
        "description": "Joins a customer to an owned event's waitlist. The bearer token must pass the public API's Ultimate plan gate, and the shop must also be Pro Plus or higher for the waitlist feature. `preferredTime` must include a real ISO 8601 timezone offset; the server stores it in UTC. The event must have waitlist status `no_timeslots` or `enabled`.",
        "additionalProperties": false,
        "required": ["name", "email", "preferredTime", "timezone", "eventId", "variantId"],
        "properties": {
          "name": { "type": "string", "minLength": 1, "maxLength": 255 },
          "email": { "type": "string", "format": "email", "maxLength": 255 },
          "preferredTime": { "type": "string", "format": "date-time", "description": "ISO 8601 date-time with an explicit offset, such as `2026-08-20T10:00:00-04:00`. Stored as UTC." },
          "timezone": { "type": "string", "description": "IANA timezone name, such as `America/Toronto`." },
          "preferredEmployeeId": { "type": ["integer", "null"], "minimum": 1, "maximum": 2147483647, "description": "Optional staff Availability ID owned by this shop." },
          "eventId": { "type": "integer", "minimum": 1, "maximum": 2147483647, "description": "Owned event with waitlist enabled." },
          "variantId": { "type": "string", "minLength": 1, "maxLength": 255, "description": "Variant configured on the selected event." }
        },
        "example": {
          "name": "Priya Sharma",
          "email": "priya.sharma@example.com",
          "preferredTime": "2026-08-20T10:00:00-04:00",
          "timezone": "America/Toronto",
          "preferredEmployeeId": 312,
          "eventId": 5117,
          "variantId": "43219874512903"
        }
      },
      "WaitlistUpdate": {
        "type": "object",
        "description": "Updates one owned waitlist entry. Send at least one field. `preferredTime` must include an explicit timezone offset and is stored in UTC.",
        "additionalProperties": false,
        "minProperties": 1,
        "properties": {
          "staffNotes": { "type": ["string", "null"], "maxLength": 255 },
          "preferredTime": { "type": "string", "format": "date-time" },
          "preferredEmployeeId": { "type": ["integer", "null"], "minimum": 1, "maximum": 2147483647, "description": "Staff Availability ID owned by this shop, or null to clear the preference." },
          "status": { "type": "string", "enum": ["waiting", "messaged", "booked", "expired"] }
        },
        "example": { "preferredTime": "2026-08-21T10:00:00-04:00", "staffNotes": "Customer can take the next weekday opening." }
      },
      "WaitlistMessage": {
        "type": "object",
        "description": "Sends an invitation message for one owned waitlist entry. The message can send email/Klaviyo notifications and a configured webhook; the legacy workflow changes `waiting` entries to `messaged`.",
        "additionalProperties": false,
        "required": ["subject", "message"],
        "properties": {
          "subject": { "type": "string", "minLength": 1, "maxLength": 255 },
          "message": { "type": "string", "minLength": 1, "maxLength": 50000 },
          "slotStartTime": { "type": ["string", "null"], "format": "date-time", "description": "Optional ISO 8601 timestamp with an explicit offset for the freed slot. Stored and used as UTC by downstream invite links." }
        },
        "example": {
          "subject": "A time is available for your appointment",
          "message": "A time has opened on August 20 at 10:00. Use the link below to book it.",
          "slotStartTime": "2026-08-20T10:00:00-04:00"
        }
      }
    }
  },
  "paths": {
    "/api/public/v1/me": {
      "get": {
        "operationId": "getCapabilities",
        "tags": ["Account"],
        "summary": "Check a token and its permissions",
        "description": "Call this endpoint first. It verifies the bearer token and returns the shop it belongs to, the credential's scopes, the shop's timezone, the API version, and the current server time. No additional scope is required. This response does not include currency. See Prices and currency for the same shop's Shopify Admin GraphQL currencyCode source and its separate authentication requirement.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/Capability" } }
            }
          },
          "401": {
            "description": "Unauthenticated",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "429": { "description": "Rate limit exceeded. Retry after the number of seconds in the Retry-After header.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/public/v1/openapi.json": {
      "get": { "operationId": "getOpenApiSpec", "tags": ["Meta"], "summary": "Get the OpenAPI specification", "description": "Returns the machine-readable OpenAPI 3.1 specification for this API. This endpoint is public and does not require a token.", "security": [], "responses": { "200": { "description": "OpenAPI specification" } } }
    },
    "/api/public/v1/docs": {
      "get": {
        "operationId": "getApiReference",
        "tags": ["Meta"],
        "summary": "Open the interactive API reference",
        "description": "Renders this OpenAPI specification as a human-readable, interactive reference. This endpoint is public and does not require a token.",
        "security": [],
        "responses": { "200": { "description": "HTML page rendering this spec" } }
      }
    },
    "/api/public/v1/reports/peak-hours": {
      "get": {
        "operationId": "getPeakHoursReport",
        "tags": ["Reports"],
        "summary": "Get peak booking hours",
        "description": "Shows when the shop receives the most bookings by grouping booking counts by local day of the week and hour. Use the result to build a peak-hours heatmap or compare demand across times of day.\n\nThe `data` array is sparse: it contains only buckets with bookings. Treat every omitted day-and-hour combination as zero, as indicated by `meta.missingBucketsAreZero`.\n\nIf you omit `from` and `to`, the report covers the last 90 local calendar days, including today. For a custom range, send both dates; the range cannot exceed 90 calendar days. Results may be cached for up to five minutes.\n\nRequires the `reports:read` scope. This endpoint is limited to 30 requests per minute per credential.",
        "security": [{ "bearerAuth": [] }],
        "parameters": [
          { "name": "from", "in": "query", "required": false, "schema": { "type": "string", "format": "date" }, "description": "First local date to include, in `YYYY-MM-DD` format. Send it with `to`, or omit both dates to report on the last 90 days." },
          { "name": "to", "in": "query", "required": false, "schema": { "type": "string", "format": "date" }, "description": "Last local date to include, in `YYYY-MM-DD` format. Send it with `from`. It cannot be earlier than `from`, and the range cannot exceed 90 calendar days." },
          { "name": "timezone", "in": "query", "required": false, "schema": { "type": "string", "example": "America/Toronto" }, "description": "IANA timezone for local date boundaries and hourly grouping. Defaults to the shop timezone, then UTC." },
          { "name": "eventId", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1 }, "description": "Restrict results to one event owned by the authenticated shop." }
        ],
        "responses": {
          "200": { "description": "Peak-hours heatmap data", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PeakHoursResponse" } } } },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Missing the reports:read scope", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "Event not found or not owned by this shop", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "description": "Invalid date range, timezone, or eventId", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "500": { "description": "Report generation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/public/v1/events": {
      "get": {
        "operationId": "listEvents",
        "tags": ["Events"],
        "summary": "List events",
        "description": "Start here when you need to create a booking or inspect the shop's services. This endpoint returns both active and inactive events in ascending ID order. Check `isActive` before continuing; inactive events reject new bookings.\n\nOmit `cursor` on the first request. When `hasMore` is `true`, pass `nextCursor` as the next request's `cursor`.\n\nRequires the `events:read` scope.",
        "parameters": [
          { "name": "cursor", "in": "query", "schema": { "type": "integer" }, "description": "Pagination cursor. Omit on the first call." },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 50, "maximum": 200 } }
        ],
        "responses": {
          "200": {
            "description": "Paginated events",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EventListResponse" } } }
          },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Missing the events:read scope", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limit exceeded. Retry after the number of seconds in the Retry-After header.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "post": {
        "operationId": "createEvent",
        "tags": ["Events"],
        "summary": "Create an event",
        "description": "Creates a bookable service on a Shopify product you already have. The API does not create products; create the product in Shopify first, then send its `productId` and the `variants` customers buy.\n\nA variant can belong to only one active event. If another active event already sells one of the variants, the request fails with `422 VARIANT_ALREADY_ASSIGNED` and names the event that holds it.\n\nStaff: omit `employees` and the API creates one availability for the event, with a schedule taken from `scheduleType` and the shop's timezone. Send `employees` to use availability that already exists; every ID must belong to the shop and must not be archived. Send an empty array to create the event with no availability.\n\n`resolution` defaults to `duration`, which puts slots back to back. The new event is active immediately, so set `isActive` to `false` if you want to finish setting it up first. Requires the `events:write` scope.",
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EventCreate" } } }
        },
        "responses": {
          "201": { "description": "Event created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Event" } } } },
          "400": { "description": "Malformed body or a field that failed validation", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Missing the events:write scope", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "locationId does not reference a Location owned by this shop", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "409": { "description": "Idempotency-Key replayed with a different body", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "description": "A variant is already sold by another active event, or an availability ID is foreign or archived", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limit exceeded. Retry after the number of seconds in the Retry-After header.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/public/v1/events/{id}": {
      "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } }],
      "get": {
        "operationId": "getEvent",
        "tags": ["Events"],
        "summary": "Get an event",
        "description": "Returns one event in the same shape `GET /events` returns. Use it to read an event's current settings before you update it. To learn what a customer can book, call `booking-options` and `slots` instead.\n\nRequires the `events:read` scope.",
        "responses": {
          "200": { "description": "The event", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Event" } } } },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Missing the events:read scope", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "Event not found or not owned by this shop", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limit exceeded. Retry after the number of seconds in the Retry-After header.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "patch": {
        "operationId": "updateEvent",
        "tags": ["Events"],
        "summary": "Update an event",
        "description": "Updates one or more supported event settings. The request body is partial, so omit fields you do not want to change. See `EventUpdate` for the writable fields.\n\nThe `employees` property contains Availability IDs, despite its name. Sending it replaces the event's eligible staff list, and every ID must belong to the authenticated shop. If you send `locationId`, it must identify an active Location owned by the shop; send `null` to clear the assignment. The API updates the event's `location` value to match the assigned Location.\n\nAn event's Shopify links are fixed when the event is created. `productId` and `variants` are not accepted here. Create a new event, or duplicate this one with `POST /events/{id}/duplicate`, to attach a different product or variants.\n\nChanges that affect scheduling clear cached slots. The app also records the update in its event audit log. Requires the `events:write` scope. Send an `Idempotency-Key` when a retry is possible.",
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EventUpdate" } } }
        },
        "responses": {
          "200": { "description": "Event updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Event" } } } },
          "400": { "description": "Malformed field, invalid cross-field combination, or locationId refers to an archived Location", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Missing the events:write scope", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "Event not found or not owned by this shop, or locationId does not reference a Location owned by this shop", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "409": { "description": "Idempotency-Key replayed with a different body", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "description": "Field value failed validation", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limit exceeded. Retry after the number of seconds in the Retry-After header.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "500": { "description": "Event update failed without applying a complete response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/public/v1/events/{id}/duplicate": {
      "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } }],
      "post": {
        "operationId": "duplicateEvent",
        "tags": ["Events"],
        "summary": "Duplicate an event",
        "description": "Copies an event's settings onto its own Shopify product and variants. Use it to roll out one configured service to more products without repeating the settings.\n\nThe copy takes the source event's schedule rules, notification settings, assigned staff, and intake questions. It does not take the source's bookings, timeslots, or ticketing settings; turn ticketing on again on the copy so it gets its own ticket variants.\n\nThe copy needs its own variants. A variant another active event already sells fails with `422 VARIANT_ALREADY_ASSIGNED`. Requires the `events:write` scope.",
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EventDuplicate" } } }
        },
        "responses": {
          "201": { "description": "Event duplicated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Event" } } } },
          "400": { "description": "Malformed body or a missing required field", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Missing the events:write scope", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "Event not found or not owned by this shop", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "409": { "description": "Idempotency-Key replayed with a different body", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "description": "A variant is already sold by another active event", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limit exceeded. Retry after the number of seconds in the Retry-After header.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/public/v1/events/{eventId}/booking-options": {
      "parameters": [{ "name": "eventId", "in": "path", "required": true, "schema": { "type": "integer", "minimum": 1 } }],
      "get": {
        "operationId": "getEventBookingOptions",
        "tags": ["Events"],
        "summary": "Get the options needed to book an event",
        "description": "Call this after choosing an event and before requesting slots. The response tells you which inputs are valid for a booking: Shopify variants, active staff assigned to the event, custom fields, customer-facing location, effective timezone, and quantity limits.\n\nA staff member appearing here is eligible for the event, but that does not guarantee a particular time is free. Use `GET /events/{eventId}/slots` for live bookable times.\n\nRequires the `events:read` scope. See Custom fields and answers for control configuration and serialization, and Prices and currency for the Shopify currency source. Read shouldGateBooking from the event before offering a customer API flow.",
        "responses": {
          "200": { "description": "Booking metadata", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BookingOptions" } } } },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Missing the events:read scope", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "Event not found or not owned by this shop", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limit exceeded. Retry after the number of seconds in the Retry-After header.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/public/v1/events/{eventId}/slots": {
      "parameters": [{ "name": "eventId", "in": "path", "required": true, "schema": { "type": "integer", "minimum": 1 } }],
      "get": {
        "operationId": "listEventSlots",
        "tags": ["Events"],
        "summary": "List available event slots",
        "description": "Call this immediately before creating a booking. The API checks staff schedules, existing bookings, event limits, quantity, and capacity. It does not check customer eligibility for shouldGateBooking events; use the standard widget for those events. It returns one result for each staff member who can accept the booking at that time.\n\nChoose one result and send its `startAt`, `availabilityId`, and `variantId` to `POST /bookings`. The API checks the slot again when you create the booking.\n\n`from` and `to` are inclusive local dates and are both required. The range cannot exceed 31 days. If the event locks its timezone, that timezone takes precedence over the `timezone` query parameter.\n\nRequires the `events:read` scope.",
        "parameters": [
          { "name": "from", "in": "query", "required": true, "schema": { "type": "string", "format": "date" } },
          { "name": "to", "in": "query", "required": true, "schema": { "type": "string", "format": "date" } },
          { "name": "timezone", "in": "query", "schema": { "type": "string" }, "description": "IANA timezone used for local date boundaries and display times. If the event has a locked timezone, the API uses that timezone instead." },
          { "name": "quantity", "in": "query", "schema": { "type": "integer", "minimum": 1, "default": 1 } },
          { "name": "availabilityId", "in": "query", "schema": { "type": "integer", "minimum": 1 }, "description": "Restrict results to one assigned availability." },
          { "name": "variantId", "in": "query", "schema": { "type": "string" }, "description": "Validate and associate one of the event's configured variants with each result." }
        ],
        "responses": {
          "200": { "description": "Live bookable slots", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BookableSlotsResponse" } } } },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Missing the events:read scope", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "Event not found or not owned by this shop", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "409": { "description": "The event is inactive or has no bookable Shopify variant", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "description": "Invalid range, timezone, quantity, variant, or availability", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limit exceeded. Retry after the number of seconds in the Retry-After header.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/public/v1/bookings": {
      "post": {
        "operationId": "createBooking",
        "tags": ["Bookings"],
        "summary": "Create a booking",
        "description": "Creates a durable reservation from a result returned by `GET /events/{eventId}/slots`. Send that result's `startAt`, `availabilityId`, and `variantId`, with the same quantity. The API checks the slot and capacity again. Use `error.code` and the Recover from booking errors table to choose a recovery action; a 409 does not always mean a stale slot.\n\nThis endpoint does not collect payment or create a checkout hold. See Paid bookings for the supported widget-to-Shopify-Checkout sequence. Gated events are unsupported for customer API booking; this endpoint does not enforce the widget's customer eligibility check or return pending approval.\n\nA `201` response means the reservation was saved. It returns the booking ID and `eventDateId`, but does not guarantee a `date` object or top-level `startAt`/`endAt`. Call `GET /bookings/{id}` with the returned ID before you build a calendar confirmation. Use `date.utcStartTimeV2` and `date.utcEndTimeV2` from that response as the saved start and end times. This read requires `bookings:read`; grant it together with `events:read` and `bookings:create` when you set up the integration. If the read fails, retry the read with the saved ID. Do not create another booking.\n\nCalendar updates, notifications, webhooks, and other configured actions can finish after the response. `sendConfirmation` defaults to `true`; set it to `false` to skip the confirmation email and SMS. Other configured actions still run.\n\nRequires `bookings:create` and a unique `Idempotency-Key` header. The complete discovery, creation, and receipt flow also needs `events:read` and `bookings:read`.",
        "parameters": [{ "name": "Idempotency-Key", "in": "header", "required": true, "schema": { "type": "string", "minLength": 1, "maxLength": 255 }, "description": "Required. Replaying the same body with the same key returns the original response." }],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BookingCreate" } } }
        },
        "responses": {
          "201": { "description": "Reservation saved. The date object and top-level startAt/endAt are not guaranteed. Read GET /bookings/{id} with bookings:read for the complete saved timeslot before calendar confirmation.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Booking"}, "example": {"id": 814203, "userId": 1042, "eventId": 5117, "eventDateId": 88421, "employeeId": 312, "type": "standard", "status": null, "productId": "7825412934182", "variantId": "43219874512903", "price": 0, "isAdminCheckout": true, "quantity": 1, "timezone": "America/Toronto", "firstName": "Priya", "lastName": "Sharma", "contactEmail": "priya.sharma@example.com", "orderId": null, "draftOrderId": null, "slug": "9b3f2a4c-7e21-4a8b-9c1d-2f5e6a8b0c11", "createdAt": "2026-08-19T18:23:11.482Z", "updatedAt": "2026-08-19T18:23:11.482Z"}}} },
          "400": { "description": "Malformed request or missing Idempotency-Key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Missing the bookings:create scope", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "Event not found or not owned by this shop", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "409": { "description": "Branch on error.code: SLOT_UNAVAILABLE, CAPACITY_EXCEEDED, CUSTOM_FIELD_REQUIRED, EVENT_INACTIVE, VARIANT_REQUIRED, AVAILABILITY_NOT_ASSIGNED, IDEMPOTENCY_IN_FLIGHT, or IDEMPOTENCY_REPLAY_MISMATCH. See Recover from booking errors for the action for each code.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "description": "Invalid event option, customer data, custom field, tag, or quantity", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limit exceeded. Retry after the number of seconds in the Retry-After header.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "500": { "description": "BOOKING_CREATE_FAILED or INTERNAL_ERROR. The write result can be uncertain. Retain the idempotency key and reconcile the result before making a new booking attempt. See Recover from booking errors.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "503": { "description": "CAPACITY_VALIDATION_FAILED. No booking was created. Wait, fetch fresh slots, and use a new idempotency key for the new attempt; the previous error can be cached.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "get": {
        "operationId": "listBookings",
        "tags": ["Bookings"],
        "summary": "List bookings",
        "description": "Returns a page of bookings for the authenticated shop. Each item uses the flat `BookingListItem` shape, which combines the fields most useful in a list from the booking, event, and timeslot. Use `GET /bookings/{id}` when you need the complete booking.\n\nPages are 1-based. Results are in `dates.results`, `dates.total` is the number of matches across all pages, and the default `limit` is 30.\n\n`queryFilter` is a JSON-encoded array. Each database-field filter has the form `[resource, field, operator, value]`. For example, `[[\"EventDates\",\"startDate\",\">=\",\"2026-05-01\"]]` returns bookings on or after May 1, 2026. URL-encode the JSON when placing it in a query string.\n\nRequires the `bookings:read` scope.",
        "parameters": [
          { "name": "page", "in": "query", "schema": { "type": "integer", "default": 1 }, "description": "1-indexed page number." },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 30 } },
          { "name": "column", "in": "query", "schema": { "type": "string", "default": "id" }, "description": "Sort column." },
          { "name": "sortDirection", "in": "query", "schema": { "type": "string", "enum": ["asc", "desc"], "default": "desc" } },
          { "name": "queryFilter", "in": "query", "schema": { "type": "string" }, "description": "JSON-encoded array of filters. Booking and date filters use `[\"Bookings\" | \"EventDates\", field, operator, value]`. Tag filters use `[\"tags\",\"bookingTagId\",\"IN\",[tagIds]]`. Text or numeric search uses `[\"search\",term]`. Location filters use `[\"Events\",\"locationId\",\"IN\" | \"IN_OR_NULL\",[locationIds]]`. Example: `[[\"EventDates\",\"startDate\",\">=\",\"2026-05-01\"]]`. URL-encode the complete JSON value." }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BookingListResponse" } } }
          },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Missing the bookings:read scope", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limit exceeded. Retry after the number of seconds in the Retry-After header.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/public/v1/bookings/{id}": {
      "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } }],
      "get": {
        "operationId": "getBooking",
        "tags": ["Bookings"],
        "summary": "Get a booking",
        "description": "Returns one complete booking from the authenticated shop. The `date` property contains its timeslot; prefer `date.utcStartTimeV2` and `date.utcEndTimeV2` for exact times.\n\nSet `includeDetails=true` when you also need the latest synchronized Shopify order and address snapshot. That extra data is returned in `details`.\n\nRequires the `bookings:read` scope. After POST /bookings succeeds, read this endpoint with the returned ID for calendar confirmation. Retry this read if it fails; do not create another reservation. status is booking/attendance state, not merchant approval.",
        "parameters": [
          { "name": "includeDetails", "in": "query", "schema": { "type": "string", "enum": ["true", "false"] }, "description": "Set to `true` to include the latest Shopify order and address snapshot in the response's `details` field." }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Booking" } } }
          },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Missing the bookings:read scope", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "Booking not found or not owned by this shop", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limit exceeded. Retry after the number of seconds in the Retry-After header.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "patch": {
        "operationId": "updateBooking",
        "tags": ["Bookings"],
        "summary": "Update a booking",
        "description": "Updates customer details, internal notes, attendance status, Shopify order links, price, or tags without moving the booking. The request body is partial; see `BookingUpdate` for the accepted fields. Unknown fields are ignored.\n\nUse the dedicated endpoints to change scheduling or workflow state: `/availability` to assign staff, `/reschedule` to change time, `/cancel` to cancel, and `/remind` to send a reminder.\n\nRequires the `bookings:write` scope. Send an `Idempotency-Key` when a retry is possible.",
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/BookingUpdate" },
              "examples": {
                "checkIn": {
                  "summary": "Mark customer as checked in",
                  "description": "Records attendance. Also triggers Shopify order tag updates downstream.",
                  "value": { "status": "checked_in" }
                },
                "updateContact": {
                  "summary": "Update the customer's contact details",
                  "description": "Use when the customer corrects their contact info after booking.",
                  "value": {
                    "firstName": "Priya",
                    "lastName": "Sharma",
                    "contactEmail": "priya.sharma@example.com",
                    "contactPhone": "+14165551234"
                  }
                },
                "addInternalNotes": {
                  "summary": "Add staff-only internal notes",
                  "description": "`internalNotes` is visible only to staff and is never shown to customers.",
                  "value": {
                    "internalNotes": "Confirmed by phone — please reserve chair 3. Prefers Maya if available."
                  }
                },
                "linkOrder": {
                  "summary": "Link the booking to a Shopify order",
                  "description": "Records a reference to an existing Shopify order, for example after payment in person. Does not collect or verify payment, create a checkout, or coordinate holds and order webhooks. Do not use this update as a public paid-checkout flow.",
                  "value": {
                    "orderId": "5481923847216",
                    "orderName": "#1042",
                    "customerId": "6712398471029"
                  }
                },
                "replaceTags": {
                  "summary": "Replace the booking's tags",
                  "description": "Replaces the booking's tags with this exact list. The API trims labels, removes duplicates, and creates new labels automatically. Send an empty array to remove every tag. Booking tags stay in the app and do not sync to Shopify order tags.",
                  "value": {
                    "tags": ["VIP", "Repeat customer"]
                  }
                },
                "setPrice": {
                  "summary": "Set the booking's price",
                  "description": "Overrides the stored booking price. Must be a non-negative number. Useful when the charged amount differs from the event's default price.",
                  "value": {
                    "price": 75
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Booking" } } }
          },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Missing the bookings:write scope", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "Booking not found or not owned by this shop", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "409": { "description": "Idempotency-Key replayed with a different body", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "description": "Invalid field value (status, price, or tags)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limit exceeded. Retry after the number of seconds in the Retry-After header.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/public/v1/bookings/{id}/availability": {
      "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } }],
      "put": {
        "operationId": "setBookingAvailability",
        "tags": ["Bookings"],
        "summary": "Assign a staff member to a booking",
        "description": "Changes the staff member assigned to a booking without changing its date or time. Send the staff member's Availability ID as `availabilityId`.\n\nThe booking and Availability must belong to the authenticated shop. The staff member must be active, assigned to the event, working on the booking date, and able to accept the booking under the event's capacity rules. Use `/reschedule` instead when the time also needs to change.\n\nRequires the `bookings:write` scope. Send an `Idempotency-Key` when a retry is possible.",
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BookingAvailabilityUpdate" } } }
        },
        "responses": {
          "200": { "description": "Availability associated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Booking" } } } },
          "400": { "description": "Malformed availabilityId", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Missing the bookings:write scope", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "Booking or availability not found or not owned by this shop", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "409": { "description": "Availability is inactive or ineligible, or the Idempotency-Key was reused with a different body", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "description": "Availability cannot serve this event on the booking date", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limit exceeded. Retry after the number of seconds in the Retry-After header.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "503": { "description": "The API could not verify current capacity, so it did not change the assignment", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/public/v1/bookings/{id}/reschedule": {
      "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } }],
      "post": {
        "operationId": "rescheduleBooking",
        "tags": ["Bookings"],
        "summary": "Reschedule a booking",
        "description": "Moves a booking to another time for the same event. Cross-event moves are not supported. You can select the destination in either of two ways:\n\n- Send an existing `eventDateId` from the same event.\n- Send `startDate`, `startTime`, `endTime`, and `timezone` to create or find a timeslot from local time.\n\nYou can also send `availabilityId` to change the assigned staff member. If you omit it, the booking keeps its current assignment. An unassigned booking must provide one.\n\nThe API checks event membership, staff eligibility, and current capacity before moving the booking. A successful reschedule can update calendars, Shopify order properties, integrations, notifications, and webhooks. If no booking still uses the original timeslot, the API may remove it.\n\nRequires the `bookings:write` scope. Send an `Idempotency-Key` to prevent a retry from repeating external actions.",
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/BookingReschedule" },
              "examples": {
                "existingTimeslot": { "summary": "Move to an existing timeslot", "value": { "eventDateId": 90312, "availabilityId": 4821, "sendRescheduleEmail": true } },
                "localDateTime": { "summary": "Create or find the target timeslot", "value": { "startDate": "2026-08-12", "startTime": "10:00", "endTime": "11:00", "timezone": "America/Toronto", "availabilityId": 4821, "sendRescheduleEmail": true, "rescheduleReason": "Customer requested a later appointment" } }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Booking rescheduled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BookingRescheduleResponse" } } } },
          "400": { "description": "Malformed or incomplete timeslot selection", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Missing the bookings:write scope", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "Booking, availability, or timeslot not found or not owned by this shop", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "409": { "description": "The timeslot has insufficient capacity, the availability is ineligible, or the Idempotency-Key was reused with a different body", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "description": "Invalid date, time, timezone, or same-event relationship", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Booking has reached the reschedule limit", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "500": { "description": "Reschedule or a required downstream operation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "503": { "description": "The API could not verify current capacity, so it did not reschedule the booking", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/public/v1/bookings/{id}/cancel": {
      "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } }],
      "post": {
        "operationId": "cancelBooking",
        "tags": ["Bookings"],
        "summary": "Cancel a booking",
        "description": "Cancels a booking while keeping it in the shop's history. The API sets `deletedAt`, removes calendar reservations, and runs the app's cancellation integrations and webhooks.\n\nThe request body is optional. By default, the API does not send a cancellation notification or request a refund. Set `sendCancellationEmail` or `sendRefund` to `true` when you want those actions. Refund eligibility still depends on the booking and payment provider.\n\nA booking created through `POST /bookings` has no Shopify order, so the API skips the refund and order-tagging steps for it. Every other cancellation action still runs.\n\nCancelling an already-cancelled booking returns `200` without running cancellation actions again.\n\nRequires the `bookings:write` scope. Send an `Idempotency-Key` to prevent retries from repeating notifications, refunds, calendar changes, or integrations.",
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "requestBody": {
          "required": false,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BookingCancel" } } }
        },
        "responses": {
          "200": { "description": "Booking cancelled; an already-cancelled booking is returned without rerunning cancellation side effects", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Booking" } } } },
          "400": { "description": "Malformed cancellation options", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Missing the bookings:write scope", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "Booking not found or not owned by this shop", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "409": { "description": "Idempotency-Key replayed with a different body", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "description": "Refund or cancellation cannot be performed for this booking", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limit exceeded. Retry after the number of seconds in the Retry-After header.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "500": { "description": "Cancellation or a required downstream operation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/public/v1/bookings/{id}/remind": {
      "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } }],
      "post": {
        "operationId": "sendBookingReminder",
        "tags": ["Bookings"],
        "summary": "Send a booking reminder",
        "description": "Sends the customer a manual reminder by email, SMS, or both, depending on the event's notification settings. This is the same action as **Remind** in the admin and does not depend on the automatic reminder schedule.\n\nA sent reminder returns `remindedAt`. If reminders are disabled or the booking is not eligible, the endpoint still returns `200`, but the response contains `message` instead of `remindedAt`. Check which property is present before marking a reminder as sent.\n\nRequires the `bookings:write` scope. Send an `Idempotency-Key` to prevent retries from sending the reminder twice.",
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "responses": {
          "200": {
            "description": "The reminder was sent, or it was skipped with an explanation.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "remindedAt": { "type": ["string", "null"], "format": "date-time", "description": "Time the reminder was sent. Present only when the API sent a reminder." },
                    "message": { "type": "string", "description": "Explanation when the reminder was skipped (e.g. reminders disabled for this event)." }
                  }
                },
                "examples": {
                  "sent": { "summary": "Reminder dispatched", "value": { "remindedAt": "2026-05-11T16:25:09.482Z" } },
                  "skipped": { "summary": "Nothing sent", "value": { "message": "Reminder notifications are not enabled for this event" } }
                }
              }
            }
          },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Missing the bookings:write scope", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "Not found or not owned by this shop", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "409": { "description": "Idempotency-Key replayed with a different body", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limit exceeded. Retry after the number of seconds in the Retry-After header.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "500": { "description": "The reminder failed to send", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/public/v1/bookings/{id}/follow-up": {
      "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } }],
      "post": {
        "operationId": "sendBookingFollowUp",
        "tags": ["Bookings"],
        "summary": "Send a booking follow-up",
        "description": "Sends the follow-up (\"thank you\") notification to the customer for this booking—the same email, SMS, Klaviyo event, and Shopify Flow trigger the app normally sends after an appointment ends. Use it to follow up early or send the follow-up again.\n\nThe request returns `200` with a `message` when no notification is sent, such as when follow-ups are disabled, the booking has no email address, or the booking was cancelled. Email and SMS settings are independent, so the response can contain both `thankedAt` and a message when another channel delivered successfully.\n\nRequires the `bookings:write` scope. Send an `Idempotency-Key` to avoid sending the same follow-up twice when retrying.",
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "responses": {
          "200": {
            "description": "Follow-up sent, or skipped with an explanatory message.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "thankedAt": { "type": ["string", "null"], "format": "date-time", "description": "Timestamp the follow-up was sent. Present when a follow-up was actually dispatched on any channel — email, SMS, or Klaviyo." },
                    "message": { "type": "string", "description": "Explanation when the follow-up email was skipped (e.g. follow-up notifications disabled for this event). Appears alongside thankedAt when another channel still delivered." }
                  }
                },
                "examples": {
                  "sent": { "summary": "Follow-up dispatched", "value": { "thankedAt": "2026-05-12T09:03:41.117Z" } },
                  "skipped": { "summary": "Nothing sent", "value": { "message": "The follow-up email was not sent because follow-up notifications are not enabled for this event." } }
                }
              }
            }
          },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Missing the bookings:write scope", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "Not found or not owned by this shop", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "409": { "description": "Idempotency-Key replayed with a different body", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limit exceeded. Retry after the number of seconds in the Retry-After header.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "500": { "description": "The follow-up failed to send.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/public/v1/waitlist": {
      "get": {
        "operationId": "listWaitlist",
        "tags": ["Waitlist"],
        "summary": "List waitlist entries",
        "description": "Returns a page of waitlist entries owned by the authenticated shop. Filters are combined. `dateFrom` and `dateTo` use UTC day boundaries, and returned `preferredTime`, `created_at`, and `updated_at` values are ISO 8601 UTC timestamps. Results can be sorted by `created_at` or `preferred_time`. Requires the `waitlist:read` scope.",
        "security": [{ "bearerAuth": [] }],
        "parameters": [
          { "name": "search", "in": "query", "schema": { "type": "string", "maxLength": 255 }, "description": "Case-insensitive match against the customer's name or email." },
          { "name": "eventId", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 2147483647 }, "description": "Restrict results to one event owned by this shop." },
          { "name": "status", "in": "query", "schema": { "type": "string", "example": "waiting,messaged" }, "description": "Comma-separated statuses: `waiting`, `messaged`, `booked`, or `expired`." },
          { "name": "dateFrom", "in": "query", "schema": { "type": "string", "format": "date" }, "description": "Include preferred times from this UTC calendar date." },
          { "name": "dateTo", "in": "query", "schema": { "type": "string", "format": "date" }, "description": "Include preferred times through this UTC calendar date." },
          { "name": "sort", "in": "query", "schema": { "type": "string", "enum": ["created_at asc", "created_at desc", "preferred_time asc", "preferred_time desc"], "default": "created_at desc" }, "description": "Sort field and direction." },
          { "name": "page", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 10000, "default": 1 } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 30 } }
        ],
        "responses": {
          "200": { "description": "A page of owned waitlist entries", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WaitlistListResponse" } } } },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "402": { "description": "The shop's plan does not allow public API access", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Missing the waitlist:read scope", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "description": "A filter is invalid or outside its bounds", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "500": { "description": "The list could not be loaded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "post": {
        "operationId": "createWaitlist",
        "tags": ["Waitlist"],
        "summary": "Join an event waitlist",
        "description": "Creates a waitlist entry for an event owned by the authenticated shop. The bearer token must pass the public API's Ultimate plan gate, and the shop must also be Pro Plus or higher for the waitlist feature. The event must have waitlist status `no_timeslots` or `enabled`. The existing join workflow looks up or creates the Shopify customer, stores the entry, sends the configured waitlist-placed email/Klaviyo event, and dispatches the configured waitlist webhook. These customer side effects can run after the row is saved, so use the same `Idempotency-Key` when retrying an uncertain result. Requires the `waitlist:write` scope.",
        "security": [{ "bearerAuth": [] }],
        "parameters": [{ "name": "Idempotency-Key", "in": "header", "required": true, "schema": { "type": "string", "minLength": 1, "maxLength": 255 }, "description": "Unique key for this logical customer join. Reuse it only to retry the identical request." }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WaitlistCreate" } } } },
        "responses": {
          "201": { "description": "Waitlist entry created; customer and webhook side effects may have been dispatched", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WaitlistEntry" } } } },
          "400": { "description": "Missing or malformed Idempotency-Key or request body", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "402": { "description": "The shop's plan does not include waitlist access", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Missing the waitlist:write scope", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "The event does not belong to this shop", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "409": { "description": "The event waitlist is disabled, or the idempotency key conflicts with another request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "description": "A field, variant, or preferred staff ID failed validation", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "500": { "description": "The entry or a required customer workflow could not be completed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/public/v1/waitlist/stats": {
      "get": {
        "operationId": "getWaitlistStats",
        "tags": ["Waitlist"],
        "summary": "Get waitlist counts",
        "description": "Returns active, booked, messaged, and expired counts for the authenticated shop. `activeCount` counts entries whose preferred time is in the future, regardless of status. Requires the `waitlist:read` scope.",
        "security": [{ "bearerAuth": [] }],
        "responses": {
          "200": { "description": "Waitlist counts", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WaitlistStats" } } } },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "402": { "description": "The shop's plan does not allow public API access", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Missing the waitlist:read scope", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "500": { "description": "The counts could not be loaded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/public/v1/waitlist/{id}": {
      "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "integer", "minimum": 1, "maximum": 2147483647 } }],
      "get": {
        "operationId": "getWaitlist",
        "tags": ["Waitlist"],
        "summary": "Retrieve a waitlist entry",
        "description": "Returns one waitlist entry owned by the authenticated shop. Missing and foreign IDs return the same `404 NOT_FOUND` response. Requires the `waitlist:read` scope.",
        "security": [{ "bearerAuth": [] }],
        "responses": {
          "200": { "description": "Owned waitlist entry", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WaitlistEntry" } } } },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "402": { "description": "The shop's plan does not allow public API access", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Missing the waitlist:read scope", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "Waitlist entry not found or not owned by this shop", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "description": "The ID is not a positive 32-bit integer", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "500": { "description": "The entry could not be loaded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "patch": {
        "operationId": "updateWaitlist",
        "tags": ["Waitlist"],
        "summary": "Update a waitlist entry",
        "description": "Updates one waitlist entry owned by the authenticated shop. Supported fields are staff notes, preferred time, preferred staff, and status. An idempotency key is optional by convention; send one when a retry is possible. Requires the `waitlist:write` scope.",
        "security": [{ "bearerAuth": [] }],
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WaitlistUpdate" } } } },
        "responses": {
          "200": { "description": "Updated owned waitlist entry", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WaitlistEntry" } } } },
          "400": { "description": "Malformed idempotency key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "402": { "description": "The shop's plan does not allow public API access", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Missing the waitlist:write scope", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "Waitlist entry or related event not found or not owned by this shop", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "409": { "description": "The idempotency key conflicts with another request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "description": "The patch is empty or contains an invalid field, timestamp, status, or staff ID", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "500": { "description": "The update could not be completed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/public/v1/waitlist/{id}/message": {
      "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "integer", "minimum": 1, "maximum": 2147483647 } }],
      "post": {
        "operationId": "sendWaitlistMessage",
        "tags": ["Waitlist"],
        "summary": "Send a waitlist invitation",
        "description": "Sends an invitation for one waitlist entry owned by the authenticated shop. The existing workflow sends email/Klaviyo notifications, updates a waiting entry to `messaged` after a successful send, and dispatches the configured waitlist webhook. Use the same required `Idempotency-Key` when retrying an uncertain customer-facing side effect. Requires the `waitlist:write` scope.",
        "security": [{ "bearerAuth": [] }],
        "parameters": [{ "name": "Idempotency-Key", "in": "header", "required": true, "schema": { "type": "string", "minLength": 1, "maxLength": 255 }, "description": "Unique key for this logical invitation. Reuse it only to retry the identical request." }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WaitlistMessage" } } } },
        "responses": {
          "200": { "description": "Invitation sent and the current entry returned", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WaitlistEntry" } } } },
          "400": { "description": "Missing or malformed Idempotency-Key or request body", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "402": { "description": "The shop's plan does not allow public API access", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Missing the waitlist:write scope", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "Waitlist entry or its event is not found or not owned by this shop", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "409": { "description": "The idempotency key conflicts with another request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "description": "The message, timestamp, or related staff ID failed validation", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "500": { "description": "The invitation could not be sent", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/public/v1/availability": {
      "get": {
        "operationId": "listAvailability",
        "tags": ["Availability"],
        "summary": "List staff availability",
        "description": "Returns every staff Availability for the authenticated shop as an unpaginated array. Each item combines a staff profile with `regularHours` and `specialHours`.\n\nThis endpoint describes staff and their configured schedules. It does not account for existing bookings, event rules, or capacity, so do not use it to promise a customer a time. Use `GET /events/{eventId}/slots` for bookable times.\n\nAuthentication secrets, OAuth credentials, linked employees, and team-login permissions are never returned.\n\nRequires the `availability:read` scope.",
        "responses": {
          "200": {
            "description": "OK",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AvailabilityListResponse" } } }
          },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Missing the availability:read scope", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limit exceeded. Retry after the number of seconds in the Retry-After header.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "post": {
        "operationId": "createAvailability",
        "tags": ["Availability"],
        "summary": "Create staff availability",
        "description": "Creates a staff profile and schedule for the authenticated shop. The server sets ownership, authentication fields, permissions, calendar credentials, archival state, and system fields.\n\nIf you send `locationId`, it must identify an active Location owned by the shop. The API sets the new Availability's `location` value to match it. Creating an Availability does not make the staff member eligible for an event. To assign them, include the new Availability ID in the event's `employees` array with `PATCH /events/{id}`. Sending `employees` replaces the event's complete staff list, so include the IDs that should remain assigned.\n\nRequires the `availability:write` scope. Send an `Idempotency-Key` when a retry is possible.",
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AvailabilityCreate" } } }
        },
        "responses": {
          "201": { "description": "Availability created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Availability" } } } },
          "400": { "description": "Malformed schedule/profile data, or locationId refers to an archived Location", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Missing the availability:write scope", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "locationId does not reference a Location owned by this shop", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "409": { "description": "Idempotency-Key replayed with a different body", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "description": "Schedule, timezone, profile data, or locationId failed validation", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limit exceeded. Retry after the number of seconds in the Retry-After header.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/public/v1/availability/{id}": {
      "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } }],
      "put": {
        "operationId": "updateAvailability",
        "tags": ["Availability"],
        "summary": "Update a staff schedule, location, or status",
        "description": "Replaces `regularHours`, `specialHours`, or both for one staff Availability. This is a replacement operation, not a merge: each schedule object you send becomes the complete value for that field. Omit a field to leave it unchanged.\n\nUse an empty array to mark a day or special date as unavailable. A `specialHours` entry replaces the regular schedule for that date; it does not add hours to it. You can also send `locationId` to assign an active Location owned by the shop, or `null` to clear it. The API records schedule history and clears cached slots for affected events.\n\nSend `isArchived: true` to retire the profile when a program ends, and `false` to bring it back. Send `isTeamLoginEnabled` to grant or withdraw Team Portal access; granting it takes one of the shop's purchased seats, and the API answers 422 `NO_SEATS_AVAILABLE` when every seat is taken. The two flags are independent, so archiving a profile leaves its seat in place until you also send `isTeamLoginEnabled: false`. A newly granted staff member sets their own password from the Team Portal sign-in page.\n\nThis endpoint cannot change profile fields such as name, email, phone, or timezone. Requires the `availability:write` scope. Send an `Idempotency-Key` when a retry is possible.",
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/AvailabilityUpdate" },
              "examples": {
                "weeklySchedule": {
                  "summary": "Replace the weekly recurring schedule",
                  "description": "Replaces `regularHours` for the standard week without changing `specialHours`. An empty array means the staff member is unavailable that day.",
                  "value": {
                    "regularHours": {
                      "Monday": ["09:00-12:00", "13:00-17:00"],
                      "Tuesday": ["09:00-17:00"],
                      "Wednesday": ["09:00-17:00"],
                      "Thursday": ["09:00-17:00"],
                      "Friday": ["09:00-15:00"],
                      "Saturday": [],
                      "Sunday": []
                    }
                  }
                },
                "holidayClosure": {
                  "summary": "Mark a specific date as closed",
                  "description": "Adds an override for one date in `specialHours`. An empty array marks the date as unavailable all day.",
                  "value": {
                    "specialHours": {
                      "2026-12-25": []
                    }
                  }
                },
                "vacationWeek": {
                  "summary": "Block out a multi-day vacation",
                  "description": "Add a separate `YYYY-MM-DD` entry for each date. Each entry overrides that date independently.",
                  "value": {
                    "specialHours": {
                      "2026-07-13": [],
                      "2026-07-14": [],
                      "2026-07-15": [],
                      "2026-07-16": [],
                      "2026-07-17": []
                    }
                  }
                },
                "earlyClose": {
                  "summary": "Shorten one day's hours",
                  "description": "A non-empty range replaces the date's normal hours; it does not extend them. This example closes at 13:00 on Christmas Eve.",
                  "value": {
                    "specialHours": {
                      "2026-12-24": ["09:00-13:00"]
                    }
                  }
                },
                "combined": {
                  "summary": "Update both regular and special hours in one call",
                  "description": "Replaces `regularHours` and `specialHours` together in one request.",
                  "value": {
                    "regularHours": {
                      "Monday": ["10:00-18:00"],
                      "Tuesday": ["10:00-18:00"],
                      "Wednesday": ["10:00-18:00"],
                      "Thursday": ["10:00-20:00"],
                      "Friday": ["10:00-20:00"],
                      "Saturday": ["11:00-16:00"],
                      "Sunday": []
                    },
                    "specialHours": {
                      "2026-12-24": ["10:00-14:00"],
                      "2026-12-25": [],
                      "2026-12-31": ["10:00-14:00"]
                    }
                  }
                },
                "locationAssignment": {
                  "summary": "Assign or clear the availability's Location",
                  "description": "Sets locationId to a Location owned by this shop; the response's location string is overwritten with that Location's customer-facing value. Pass null to clear the assignment.",
                  "value": { "locationId": 91 }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Availability" } } }
          },
          "400": { "description": "The assigned Location is archived", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Missing the availability:write scope", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "Availability or Location not found, or not owned by this shop", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "409": { "description": "Idempotency-Key replayed with a different body", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "description": "Schedule shape, date, time range, or locationId failed validation", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limit exceeded. Retry after the number of seconds in the Retry-After header.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/public/v1/locations": {
      "get": {
        "operationId": "listLocations",
        "tags": ["Locations"],
        "summary": "List locations",
        "description": "Returns every active Location for the authenticated shop. Set `includeArchived=true` to include archived Locations.\n\nTo assign a Location, send its `id` as `locationId` when updating an Event or creating or updating an Availability. Requires the `locations:read` scope.",
        "parameters": [
          { "name": "includeArchived", "in": "query", "schema": { "type": "boolean", "default": false }, "description": "When true, includes archived locations. Archived locations can still be referenced by locationId on existing assignments." }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LocationListResponse" } } }
          },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Missing the locations:read scope", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limit exceeded. Retry after the number of seconds in the Retry-After header.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "post": {
        "operationId": "createLocation",
        "tags": ["Locations"],
        "summary": "Create a location",
        "description": "Creates a Location for the authenticated shop and returns it. Send `shopifyLocationId` to map it to a Shopify location in the same request.\n\nAssign the returned `id` as `locationId` on an Event or an Availability. Requires the `locations:write` scope. Send an `Idempotency-Key` when a retry is possible.",
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LocationCreate" } } }
        },
        "responses": {
          "201": { "description": "Created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Location" } } } },
          "409": { "description": "That Shopify location is already mapped to another location on this shop.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Missing the locations:write scope", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "description": "The body failed validation.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limit exceeded. Retry after the number of seconds in the Retry-After header.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/public/v1/locations/{id}": {
      "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } }],
      "get": {
        "operationId": "getLocation",
        "tags": ["Locations"],
        "summary": "Retrieve a location",
        "description": "Returns one Location owned by the authenticated shop, including archived Locations. Requires the `locations:read` scope.",
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Location" } } } },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Missing the locations:read scope", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "Location not found or not owned by this shop", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limit exceeded. Retry after the number of seconds in the Retry-After header.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "patch": {
        "operationId": "updateLocation",
        "tags": ["Locations"],
        "summary": "Update a location",
        "description": "Changes the writable fields of one Location owned by the authenticated shop. Omitted fields keep their stored value.\n\nA new `name` or `address` also updates the place shown on every event and staff profile assigned to this Location. Past bookings keep the place recorded at the time they were made.\n\n`shopifyLocationId` maps this Location to a Shopify location, or unmaps it when sent as `null`. A Shopify location belongs to one Location per shop, so mapping one another Location already holds moves it. An archived Location can be unmapped but not mapped. Requires the `locations:write` scope. Send an `Idempotency-Key` when a retry is possible.",
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LocationUpdate" } } }
        },
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Location" } } } },
          "409": { "description": "That Shopify location is already mapped to another location on this shop.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Missing the locations:write scope", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "Location not found or not owned by this shop", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "description": "The body failed validation.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limit exceeded. Retry after the number of seconds in the Retry-After header.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    }
  }
}
