{
  "openapi": "3.0.3",
  "info": {
    "title": "MeshHub Merchant Supplier API",
    "version": "2.9.0",
    "description": "The API a merchant implements so MeshHub can sell their services —\nthe machine-readable companion to the MeshHub Merchant Supplier API\nspecification. **You implement this API; MeshHub calls it.** It is the\nmirror image of the tenant API at `/api/v1/`, which MeshHub implements\nand tenants call — do not confuse the two.\n\nGenerate a server skeleton from this file in any language, then read the\nprose spec for the rules a schema cannot express: offer-id signing,\nbooking idempotency, the status transition graph, and the bounded\ndefault search window.\n\nThe prose specification is normative. Where this file and the spec\ndisagree, the spec wins — report the mismatch to MeshHub.",
    "contact": {
      "name": "MeshHub merchant integrations",
      "email": "hello@meshhub.ai"
    }
  },
  "externalDocs": {
    "description": "Merchant Supplier API specification (normative)",
    "url": "https://merchant.meshhub.ai/docs/merchant-api-spec.md"
  },
  "servers": [
    {
      "url": "{base_url}/api/supplier/v1",
      "description": "Your implementation. The paths below are the standard defaults; you may register a custom URL per operation at onboarding (spec §3). Methods, payloads and semantics are not negotiable — only the URLs.",
      "variables": {
        "base_url": {
          "default": "https://staging.your.host",
          "description": "Your HTTPS base URL. Plain HTTP is rejected at registration."
        }
      }
    }
  ],
  "security": [
    {
      "ApiKeyAuth": []
    }
  ],
  "tags": [
    {
      "name": "liveness",
      "description": "Proves the auth, throttle and envelope path works."
    },
    {
      "name": "availability",
      "description": "Live search and re-quote."
    },
    {
      "name": "reservations",
      "description": "Create, read and cancel reservations."
    },
    {
      "name": "catalogue",
      "description": "What you front, rather than what is bookable right now."
    }
  ],
  "paths": {
    "/ping/": {
      "get": {
        "operationId": "ping",
        "tags": [
          "liveness"
        ],
        "summary": "Liveness (Tier 1, mandatory)",
        "description": "Authenticated no-op. An invalid key here MUST produce the standard\n401 `AUTH_FAILED` envelope — the conformance suite checks both the\nvalid and the invalid key.",
        "responses": {
          "200": {
            "description": "Alive and authenticated.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status"
                  ],
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "ok"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AuthFailed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/search/": {
      "get": {
        "operationId": "search",
        "tags": [
          "availability"
        ],
        "summary": "Live availability (Tier 1, mandatory)",
        "description": "Returns bookable offers. **One offer = one bookable combination** —\na service, performed by a specific staff member, at a specific place\n— carrying every free time it can be booked at in its `slots` array.\nAvailability is computed by you at the moment of the call; there is\nno inventory push and no cache.\n\nAn offer with no free time left is not returned at all, rather than\nreturned with an empty `slots`. An empty result set is `200` with\n`\"results\": []` — never an error.\n\nWith no date parameter, return a bounded near-term default window of\nyour choosing: starting today, spanning at most 7 days.\n\nMeshHub calls this with a 15 second timeout.",
        "parameters": [
          {
            "name": "date",
            "in": "query",
            "description": "Salon-like family only (spec §9.1), where it is mandatory to honour. Only slots on this day. **Mandatory to honour.**",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "date_start",
            "in": "query",
            "description": "Salon-like family only (spec §9.1), where it is mandatory to honour. Start of an inclusive date range. **Mandatory to honour.**",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "date_end",
            "in": "query",
            "description": "Salon-like family only (spec §9.1), where it is mandatory to honour. End of an inclusive date range. **Mandatory to honour.** Earlier than `date_start` → `VALIDATION`.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "master_id",
            "in": "query",
            "description": "Salon-like family only (spec §9.1), where it is mandatory to honour. Only offers for this staff member or resource. **Mandatory to honour.**",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "partner_id",
            "in": "query",
            "description": "Salon-like family only (spec §9.1), where it is mandatory for aggregators. Reused by the hotel family to carry `hotel_codes` (spec §9.5). Only offers at this place. **Mandatory for aggregators** — any merchant fronting more than one salon, branch or location. A single-location merchant has one partner and MAY ignore it.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "category",
            "in": "query",
            "description": "A MeshHub category slug, sent on every search call. See spec §9 for the current list of declarable slugs — deliberately not enumerated here so this file cannot drift from the taxonomy. **Mandatory to honour for multi-category merchants**: return only offers in that category. A slug you declared but have no inventory for, or one you never declared, gets an empty `results`, NOT an error. A merchant registered for a single category only ever receives that one value and MAY ignore the param. A request without the param means \"no restriction\".",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Free text: service, staff, place or category name. Optional to support.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "category_id",
            "in": "query",
            "description": "Restrict to one of *your* service categories (your internal taxonomy, not MeshHub's). Optional to support.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "city",
            "in": "query",
            "description": "Salon-like (spec §9.1) and car rental (§9.4, where it is mandatory to honour alongside `pickup_point`). Location filter, contains, case-insensitive. Optional to support.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "master_name",
            "in": "query",
            "description": "Only offers whose staff member's name contains this, case-insensitive. Optional to support — MeshHub also narrows by staff name itself, so ignoring it costs wasted payload, not correctness.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "page_size",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "pickup_point",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Car rental (spec §9.4), mandatory to honour. Branch or desk within the city; must be satisfied by the SAME location row as `city`."
          },
          {
            "name": "vehicle_class",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Car rental (spec §9.4). Your class name."
          },
          {
            "name": "transmission",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "manual",
                "automatic"
              ]
            },
            "description": "Car rental (spec §9.4)."
          },
          {
            "name": "min_seats",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "Car rental (spec §9.4). Minimum seats."
          },
          {
            "name": "min_doors",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "Car rental (spec §9.4). Minimum doors."
          },
          {
            "name": "min_small_bags",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "Car rental (spec §9.4). Minimum cabin bags."
          },
          {
            "name": "min_large_bags",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "Car rental (spec §9.4). Minimum hold bags."
          },
          {
            "name": "unlimited_mileage",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean"
            },
            "description": "Car rental (spec §9.4). Sent on `false` too — 'only vehicles without unlimited mileage' is a real ask."
          },
          {
            "name": "insurance_type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Car rental (spec §9.4). Your insurance package name."
          },
          {
            "name": "currency",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Car rental (spec §9.4). Restrict to this currency."
          },
          {
            "name": "cities",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Car rental (spec §9.4). Comma-separated acceptable collection cities."
          },
          {
            "name": "destination",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Hotel (spec §9.5). Destination or city."
          },
          {
            "name": "check_in",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Hotel (spec §9.5), mandatory to honour. First night."
          },
          {
            "name": "check_out",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Hotel (spec §9.5), mandatory to honour. Departure day; a stay is [check_in, check_out)."
          },
          {
            "name": "adults",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "Hotel (spec §9.5). Adults to price for."
          },
          {
            "name": "children",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "Hotel (spec §9.5). Children to price for."
          },
          {
            "name": "rooms",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "Hotel (spec §9.5). Rooms to price for."
          },
          {
            "name": "child_ages",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Hotel (spec §9.5). Comma-separated child ages."
          },
          {
            "name": "origin",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Travel (spec §9.6) and shipping (§9.7), mandatory to honour. IATA airport code, or ISO-3166-1 alpha-2 country for shipping."
          },
          {
            "name": "trip_type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Travel (spec §9.6). One-way or return, in your own vocabulary."
          },
          {
            "name": "cabin",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Travel (spec §9.6). Cabin class, in your own vocabulary."
          },
          {
            "name": "airlines",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Travel (spec §9.6). Restrict to these carriers."
          },
          {
            "name": "flexibility",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Travel (spec §9.6). How far around the dates to look."
          },
          {
            "name": "infants",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "Travel (spec §9.6). Infant passengers."
          },
          {
            "name": "passenger_ages",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Travel (spec §9.6). Comma-separated ages for passengers whose age was stated rather than a type."
          },
          {
            "name": "weight_kg",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Shipping (spec §9.7), mandatory to honour. Decimal string, kilograms."
          },
          {
            "name": "dim_w",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number"
            },
            "description": "Shipping (spec §9.7). Parcel width, cm."
          },
          {
            "name": "dim_h",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number"
            },
            "description": "Shipping (spec §9.7). Parcel height, cm."
          },
          {
            "name": "dim_l",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number"
            },
            "description": "Shipping (spec §9.7). Parcel length, cm."
          },
          {
            "name": "package_type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Shipping (spec §9.7). Your package type."
          },
          {
            "name": "transportation_type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Shipping (spec §9.7). Your service mode."
          }
        ],
        "responses": {
          "200": {
            "description": "Matching offers, possibly none.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SearchResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/AuthFailed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/price/": {
      "post": {
        "operationId": "price",
        "tags": [
          "availability"
        ],
        "summary": "Re-quote (Tier 2, recommended)",
        "description": "Dry run of `book`: decode the offer, re-validate the requested slot,\nreturn the current price. A stale offer MUST surface\n`OFFER_EXPIRED` / `SLOT_TAKEN` here exactly as `book` would, so\nMeshHub can re-search *before* attempting to book.\n\nOptional to implement. MeshHub books more confidently with it.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PriceRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Current quote.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PriceResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/AuthFailed"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/book/": {
      "post": {
        "operationId": "book",
        "tags": [
          "reservations"
        ],
        "summary": "Create the reservation (Tier 1, mandatory)",
        "description": "Decode and re-validate the offer, then create the reservation and\nblock the slot in your calendar.\n\n**Idempotency (MUST).** Derive a stable key from the offer's\n*contents* plus `appointment_at` — for example\n`meshhub:{service_id}:{master_id}:{appointment_at}` — not from the\nsigned token, which may differ per issuance. One offer covers several\nslots, so the token alone is not a unique booking: two bookings of the\nsame offer at different times are two orders, not a replay. If an\norder with that key already exists, return it with HTTP **200**\ninstead of creating a duplicate; first creation returns HTTP **201**.\nHandle the concurrent-duplicate race with a unique constraint and a\ncatch — the conformance suite books the same offer *and* time twice\nand requires the same `supplier_order_id` back.\n\nMeshHub calls this with a 30 second timeout.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BookRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Reservation created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Order"
                }
              }
            }
          },
          "200": {
            "description": "Idempotent replay — the order already existed for this offer and time. Body is identical to the 201.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Order"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/AuthFailed"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/booking/{supplier_order_id}/": {
      "parameters": [
        {
          "$ref": "#/components/parameters/SupplierOrderId"
        }
      ],
      "get": {
        "operationId": "bookingStatus",
        "tags": [
          "reservations"
        ],
        "summary": "Reservation status (Tier 1, mandatory)",
        "description": "MeshHub polls this after booking. There are no webhooks in this spec\nversion.\n\n**Scope check (MUST).** Only orders created through this API may be\nreadable here. Ids from your other channels — web, app, walk-in —\nMUST return `NOT_FOUND`, so MeshHub can never probe them, especially\nif your ids are sequential.",
        "responses": {
          "200": {
            "description": "Current state of the reservation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Order"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AuthFailed"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/booking/{supplier_order_id}/cancel/": {
      "parameters": [
        {
          "$ref": "#/components/parameters/SupplierOrderId"
        }
      ],
      "post": {
        "operationId": "bookingCancel",
        "tags": [
          "reservations"
        ],
        "summary": "Cancel a reservation (Tier 1, mandatory)",
        "description": "No request body. Cancels the reservation and frees the slot in your\ncalendar.\n\n**Idempotent (MUST):** cancelling an already-cancelled order returns\nHTTP 200 with its current state, not an error. Cancelling a booking\nwhose start time has passed → `VALIDATION`. Same scope check as the\nstatus endpoint.",
        "responses": {
          "200": {
            "description": "Cancelled. Body is the order with `status` now `cancelled`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Order"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/AuthFailed"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/partners/": {
      "get": {
        "operationId": "partners",
        "tags": [
          "catalogue"
        ],
        "summary": "List the businesses you front (Tier 3, optional)",
        "description": "Aggregators only — a merchant fronting more than one salon, branch\nor location. A single-location merchant can skip this endpoint\nentirely.\n\nEach entry is exactly the `partner` block already returned inside a\nsearch offer. `id` MUST be the same value you accept as the\n`partner_id` search parameter; a partner listed here and not accepted\nthere is worse than not implementing the endpoint. List every business\nyou are willing to sell, including ones with no current availability —\nthis is a directory, not a search.\n\n**Nothing depends on this endpoint.** It is not part of activation, and\nthe conformance suite skips it when absent: a `404` or `405` is a\nlegitimate answer.",
        "responses": {
          "200": {
            "description": "Your businesses.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnersResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AuthFailed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Api-Key",
        "description": "The key **you** issue to MeshHub. You authenticate MeshHub, not the other way around. Missing or wrong key → 401 with the `AUTH_FAILED` envelope. Store keys hashed at rest if you can."
      }
    },
    "parameters": {
      "SupplierOrderId": {
        "name": "supplier_order_id",
        "in": "path",
        "required": true,
        "description": "The id you returned from `book`.",
        "schema": {
          "type": "string",
          "maxLength": 255
        }
      }
    },
    "responses": {
      "AuthFailed": {
        "description": "Missing or invalid API key (`AUTH_FAILED`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "code": "AUTH_FAILED",
              "message": "Invalid API key.",
              "detail": {}
            }
          }
        }
      },
      "Validation": {
        "description": "Malformed or missing fields, or a business rule such as a slot in the past or inside your minimum booking notice (`VALIDATION`). MeshHub surfaces this to the buyer as correctable input, so put the per-field reason in `detail`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "code": "VALIDATION",
              "message": "Invalid input.",
              "detail": {
                "appointment_at": "Datetime must carry a UTC offset."
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "Unknown `supplier_order_id`, an id from another channel, or an offer whose underlying service no longer exists (`NOT_FOUND`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Conflict": {
        "description": "`OFFER_EXPIRED` — the `offer_id` is stale, tampered or past its validity; or `SLOT_TAKEN` — the slot went while MeshHub was booking it. Either way MeshHub re-runs search.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "code": "SLOT_TAKEN",
              "message": "That time slot is no longer available.",
              "detail": {}
            }
          }
        }
      },
      "RateLimited": {
        "description": "Throttled (`RATE_LIMITED`). Allow MeshHub at least 120 requests/minute. A `Retry-After` header is respected.",
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait before retrying.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "ServerError": {
        "description": "Anything unexpected (`ERROR`). MeshHub retries with backoff; sustained failures page operators.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "Every non-2xx response MUST use this body. Do not invent new codes — map internal errors to the closest reserved one, falling back to `ERROR`.",
        "required": [
          "code",
          "message"
        ],
        "properties": {
          "code": {
            "type": "string",
            "enum": [
              "AUTH_FAILED",
              "VALIDATION",
              "NOT_FOUND",
              "OFFER_EXPIRED",
              "SLOT_TAKEN",
              "RATE_LIMITED",
              "ERROR"
            ]
          },
          "message": {
            "type": "string",
            "description": "Human-readable."
          },
          "detail": {
            "type": "object",
            "additionalProperties": true,
            "description": "Optional structured context, e.g. per-field validation errors. `{}` when unused. MeshHub forwards per-field reasons to the buyer, so a `VALIDATION` without them costs the buyer their chance to correct it.",
            "default": {}
          }
        }
      },
      "Money": {
        "type": "string",
        "pattern": "^[0-9]+(\\.[0-9]{1,2})?$",
        "description": "Decimal **string** — `\"25.00\"`, never the float `25.0`.",
        "example": "25.00"
      },
      "Currency": {
        "type": "string",
        "enum": [
          "EUR",
          "USD",
          "GBP"
        ],
        "description": "ISO-4217. This spec version supports these three."
      },
      "OffsetDateTime": {
        "type": "string",
        "format": "date-time",
        "description": "ISO-8601 **with a UTC offset**, e.g. `2026-08-01T14:30:00+03:00` or `2026-08-01T11:30:00Z`. Naive local timestamps are not compliant and are rejected.",
        "example": "2026-08-01T14:30:00+03:00"
      },
      "OrderStatus": {
        "type": "string",
        "description": "MeshHub's order vocabulary. Map your internal statuses onto exactly these. A status may repeat across polls but MUST never move backwards — `confirmed` never returns to `pending`. Tolerated transitions: `pending → confirmed → paid`, any state → `cancelled`, `pending → failed`.",
        "enum": [
          "pending",
          "confirmed",
          "paid",
          "cancelled",
          "failed",
          "unknown"
        ]
      },
      "Slot": {
        "type": "object",
        "description": "One free time this combination can be booked at.",
        "required": [
          "time_start",
          "time_end"
        ],
        "properties": {
          "time_start": {
            "$ref": "#/components/schemas/OffsetDateTime"
          },
          "time_end": {
            "$ref": "#/components/schemas/OffsetDateTime"
          }
        }
      },
      "Master": {
        "type": "object",
        "description": "The staff member or resource performing the service. Part of the salon-like family block. Include what you have; `null` for missing optional sub-fields. The buyer's `staff.image_url` is filled from `image_url`, else `avatar`, else `photo` — all absolute URLs, and only ones that actually resolve, since a 404 renders as a broken image on the buyer's card. Prefer sending `avatar` as a listing-sized thumbnail (roughly 250px) (spec §9.1).",
        "required": [
          "id",
          "name"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "photo": {
            "type": "string",
            "nullable": true
          },
          "avatar": {
            "type": "string",
            "nullable": true
          },
          "image_url": {
            "type": "string",
            "nullable": true
          },
          "rating": {
            "type": "number",
            "nullable": true
          },
          "rating_count": {
            "type": "integer",
            "nullable": true
          }
        }
      },
      "Service": {
        "type": "object",
        "description": "What is being performed. Part of the salon-like family block. `category_id` and `category` are *your* internal taxonomy — MeshHub never maps its own category slugs onto them (spec §9.1).",
        "required": [
          "id",
          "name"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "description": "Becomes the buyer's offer `title`."
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "category_id": {
            "type": "string",
            "nullable": true
          },
          "category": {
            "type": "string",
            "nullable": true,
            "description": "Becomes the buyer's `service_category`."
          },
          "duration_minutes": {
            "type": "integer",
            "nullable": true
          },
          "price": {
            "$ref": "#/components/schemas/Money"
          }
        }
      },
      "Partner": {
        "type": "object",
        "description": "Where the service happens. Part of the salon-like family block. `description` and `image_url` have no other source, so a place that wants a blurb and a picture on the buyer's card has to send them here (spec §9.1).",
        "required": [
          "id",
          "name"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "description": "Becomes the buyer's `supplier_name`."
          },
          "city": {
            "type": "string",
            "nullable": true
          },
          "address": {
            "type": "string",
            "nullable": true
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "image_url": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "Offer": {
        "type": "object",
        "description": "One bookable offer. The fields listed as required here are the generic core, identical for every category family (spec §7.2). Which additional block an offer carries depends on the family of the category it was requested under — see SalonLikeOffer, CarRentalOffer, HotelOffer, TravelOffer and ShippingOffer, and spec §9.1 and §9.4-§9.7. There is deliberately no self-declared family field on the offer: MeshHub already knows the category it scoped the request to.",
        "required": [
          "offer_id",
          "price",
          "currency",
          "required_fields"
        ],
        "properties": {
          "offer_id": {
            "type": "string",
            "maxLength": 255,
            "description": "Opaque token that must survive a round trip to `price` and `book`. **Sign it** (HMAC or equivalent) so a tampered token is detectable → `OFFER_EXPIRED`. Encode whatever you need to reconstruct the combination; never trust the client's copy. It does not encode a time — `appointment_at` carries that."
          },
          "slots": {
            "type": "array",
            "description": "Sent by the salon-like family (spec §9.1) and by no other family. Ascending order. An offer with no free time left is not returned at all rather than returned with an empty array.",
            "items": {
              "$ref": "#/components/schemas/Slot"
            }
          },
          "duration_minutes": {
            "type": "integer",
            "nullable": true
          },
          "price": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Money"
              }
            ],
            "description": "MUST be greater than zero. A service you cannot state a price for is not an offer — do not return it. Beware a price column defaulting to `0`: on the wire that is indistinguishable from \"free\", and MeshHub can sell neither. **Offers priced at or below zero are dropped**, so returning them only costs you the search work."
          },
          "currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "expires_at": {
            "allOf": [
              {
                "$ref": "#/components/schemas/OffsetDateTime"
              }
            ],
            "nullable": true,
            "description": "When the offer stops being bookable, if you implement holds or a TTL. `null` means re-validated at book time — the normal case."
          },
          "required_fields": {
            "type": "array",
            "description": "What `book` needs. `end_user.*` entries go in the `end_user` object; `extra.*` entries go in the `extra` object and are named by the offer's family (spec §9.4-§9.7).",
            "items": {
              "type": "string",
              "enum": [
                "end_user.first_name",
                "end_user.last_name",
                "end_user.email",
                "end_user.phone",
                "extra.appointment_at",
                "extra.date_from",
                "extra.date_to"
              ]
            }
          },
          "master": {
            "$ref": "#/components/schemas/Master"
          },
          "service": {
            "$ref": "#/components/schemas/Service"
          },
          "partner": {
            "$ref": "#/components/schemas/Partner"
          },
          "price_unit": {
            "type": "string",
            "enum": [
              "total",
              "per_day"
            ],
            "description": "What `price` counts; `total` when absent. Car-rental offers MUST send `per_day` (spec §9.4) — a vehicle is quoted at a daily rate and the window is agreed at book. Order bodies are always totals; this is an offer field only."
          },
          "vehicle": {
            "$ref": "#/components/schemas/Vehicle"
          },
          "hotel": {
            "$ref": "#/components/schemas/Property"
          },
          "room": {
            "$ref": "#/components/schemas/Room"
          },
          "rate": {
            "$ref": "#/components/schemas/Rate"
          },
          "stay": {
            "$ref": "#/components/schemas/Stay"
          },
          "flight": {
            "$ref": "#/components/schemas/Flight"
          },
          "shipment": {
            "$ref": "#/components/schemas/Shipment"
          }
        }
      },
      "SearchResponse": {
        "type": "object",
        "required": [
          "count",
          "page",
          "page_size",
          "results"
        ],
        "properties": {
          "count": {
            "type": "integer",
            "description": "Total matching offers across all pages. MAY be a cheap upper bound, but MUST be >= the offers actually returned across all pages. `results` and page exhaustion are authoritative for what exists; MeshHub never surfaces `count` to buyers."
          },
          "page": {
            "type": "integer"
          },
          "page_size": {
            "type": "integer"
          },
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Offer"
            },
            "description": "The offers. Each one's shape is its category's family shape — see SalonLikeOffer (spec §9.1), CarRentalOffer (§9.4), HotelOffer (§9.5), TravelOffer (§9.6) and ShippingOffer (§9.7). Typed as the generic Offer here rather than as a union: an offer carries no field naming its own family (MeshHub knows the category it asked under), so there is nothing for a discriminator to key on, and an untagged oneOf would make generated clients worse for no gain. The prose spec is normative."
          }
        }
      },
      "PartnersResponse": {
        "type": "object",
        "description": "Every business you are willing to sell through MeshHub, including ones with no current availability. A directory, not a search: a fully-booked salon still exists.",
        "required": [
          "partners"
        ],
        "properties": {
          "partners": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Partner"
            }
          }
        }
      },
      "EndUser": {
        "type": "object",
        "description": "The customer. Carries the fields you declared in the offer's `required_fields`; treat the others as optional.",
        "properties": {
          "first_name": {
            "type": "string"
          },
          "last_name": {
            "type": "string"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "phone": {
            "type": "string"
          }
        }
      },
      "PriceRequest": {
        "type": "object",
        "required": [
          "offer_id"
        ],
        "properties": {
          "offer_id": {
            "type": "string",
            "maxLength": 255
          },
          "appointment_at": {
            "allOf": [
              {
                "$ref": "#/components/schemas/OffsetDateTime"
              }
            ],
            "description": "Salon-like family only (spec §9.1): which of the offer's slots this is for, matched against `slots[].time_start`. No other family sends it — see `extra`."
          },
          "extra": {
            "type": "object",
            "description": "The family-specific part of the request, where the offer alone does not identify one bookable thing. Its shape is fixed by the family of the category the offer came from: absent for salon-like (which uses `appointment_at`), `{date_from, date_to}` for car rental (spec §9.4), the guest and holder block for hotel (§9.5), `{passengers}` for travel (§9.6), and the shipper / consignee / parcels block for shipping (§9.7). The offer's `required_fields` names which of its keys are required."
          }
        }
      },
      "PriceResponse": {
        "type": "object",
        "required": [
          "offer_id",
          "price",
          "currency"
        ],
        "properties": {
          "offer_id": {
            "type": "string"
          },
          "price": {
            "$ref": "#/components/schemas/Money"
          },
          "currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "breakdown": {
            "type": "array",
            "description": "Line amounts MUST sum to `price`. A flat price is one line.",
            "items": {
              "type": "object",
              "required": [
                "label",
                "amount"
              ],
              "properties": {
                "label": {
                  "type": "string"
                },
                "amount": {
                  "$ref": "#/components/schemas/Money"
                }
              }
            }
          },
          "valid_until": {
            "allOf": [
              {
                "$ref": "#/components/schemas/OffsetDateTime"
              }
            ],
            "nullable": true,
            "description": "Latest moment the offer can still be booked, e.g. slot start minus your minimum booking notice."
          }
        }
      },
      "BookRequest": {
        "type": "object",
        "required": [
          "offer_id",
          "end_user"
        ],
        "properties": {
          "offer_id": {
            "type": "string",
            "maxLength": 255
          },
          "appointment_at": {
            "allOf": [
              {
                "$ref": "#/components/schemas/OffsetDateTime"
              }
            ],
            "description": "Salon-like family only (spec §9.1): which of the offer's slots this is for, matched against `slots[].time_start`. No other family sends it — see `extra`."
          },
          "client_ref": {
            "type": "string",
            "description": "MeshHub's idempotency reference. You MAY ignore it if your own idempotency key already satisfies the replay rule; if you store it, replays with the same `client_ref` MUST return the same order."
          },
          "end_user": {
            "$ref": "#/components/schemas/EndUser"
          },
          "extra": {
            "type": "object",
            "description": "The family-specific part of the request, where the offer alone does not identify one bookable thing. Its shape is fixed by the family of the category the offer came from: absent for salon-like (which uses `appointment_at`), `{date_from, date_to}` for car rental (spec §9.4), the guest and holder block for hotel (§9.5), `{passengers}` for travel (§9.6), and the shipper / consignee / parcels block for shipping (§9.7). The offer's `required_fields` names which of its keys are required."
          }
        }
      },
      "Order": {
        "type": "object",
        "description": "The reservation, as returned by `book`, the status endpoint and `cancel`.",
        "required": [
          "supplier_order_id",
          "status"
        ],
        "properties": {
          "supplier_order_id": {
            "type": "string",
            "maxLength": 255,
            "description": "Your id for the reservation. If your internal ids are integers, serialize them as strings."
          },
          "status": {
            "$ref": "#/components/schemas/OrderStatus"
          },
          "price": {
            "$ref": "#/components/schemas/Money"
          },
          "currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "expires_at": {
            "allOf": [
              {
                "$ref": "#/components/schemas/OffsetDateTime"
              }
            ],
            "nullable": true
          },
          "extra": {
            "type": "object",
            "nullable": true,
            "description": "Optional family-shaped detail of the reservation, surfaced to the buyer as sent: the rented window, an airline record locator, an assigned locker, a tracking number or a label URL. MeshHub polls `booking/{supplier_order_id}/`; there is no event push in this spec revision."
          }
        }
      },
      "RentalLocation": {
        "type": "object",
        "description": "One place a vehicle can be collected from. Part of the car-rental family block (spec §9.4). MeshHub matches a buyer's city and pickup_point against ONE AND THE SAME row, so a vehicle whose only branch in a city is the airport is not offered for the centre.",
        "required": [
          "city"
        ],
        "properties": {
          "city": {
            "type": "string",
            "description": "Collection city."
          },
          "pickup_point": {
            "type": "string",
            "description": "Branch or desk name within the city."
          }
        }
      },
      "Vehicle": {
        "type": "object",
        "description": "The car-rental family block (spec §9.4). `price` on the offer is this vehicle's DAILY rate. The `book` total must be that rate × days, days = date_to − date_from in calendar days, minimum one; MeshHub refuses a total more than 5 % away.",
        "required": [
          "id",
          "name"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Your identifier for the vehicle or group."
          },
          "name": {
            "type": "string",
            "description": "What you call it, e.g. 'VW Golf or similar'."
          },
          "example_model": {
            "type": "string",
            "description": "A representative model.",
            "nullable": true
          },
          "vehicle_class": {
            "type": "string",
            "description": "Your class name, e.g. 'Compact'."
          },
          "seats": {
            "type": "integer",
            "description": "Seat count.",
            "nullable": true
          },
          "doors": {
            "type": "integer",
            "description": "Door count.",
            "nullable": true
          },
          "small_bags": {
            "type": "integer",
            "description": "Cabin bags it takes.",
            "nullable": true
          },
          "large_bags": {
            "type": "integer",
            "description": "Hold bags it takes.",
            "nullable": true
          },
          "transmission": {
            "type": "string",
            "description": "Transmission.",
            "enum": [
              "manual",
              "automatic"
            ],
            "nullable": true
          },
          "unlimited_mileage": {
            "type": "boolean",
            "description": "Whether mileage is unlimited.",
            "nullable": true
          },
          "insurance_type": {
            "type": "string",
            "description": "Your insurance package name.",
            "nullable": true
          },
          "included_liability_amount": {
            "type": "string",
            "description": "Decimal string; excess covered.",
            "nullable": true
          },
          "image_url": {
            "type": "string",
            "description": "Photo URL.",
            "format": "uri",
            "nullable": true
          },
          "locations": {
            "type": "array",
            "description": "Where it can be collected.",
            "items": {
              "$ref": "#/components/schemas/RentalLocation"
            }
          }
        }
      },
      "Property": {
        "type": "object",
        "description": "The hotel itself. Part of the hotel family block (spec §9.5). There is no offer-details endpoint, so anything a rich card needs — a description, a photo — has to arrive here.",
        "required": [
          "code",
          "name"
        ],
        "properties": {
          "code": {
            "type": "string",
            "description": "Your identifier for the hotel."
          },
          "name": {
            "type": "string",
            "description": "Hotel name."
          },
          "category": {
            "type": "string",
            "description": "Your category label, e.g. '4 EST'.",
            "nullable": true
          },
          "star_rating": {
            "type": "integer",
            "description": "Stars, when you express one.",
            "nullable": true
          },
          "destination": {
            "type": "string",
            "description": "Destination or city name.",
            "nullable": true
          },
          "zone": {
            "type": "string",
            "description": "District within the destination.",
            "nullable": true
          },
          "latitude": {
            "type": "number",
            "description": "Latitude.",
            "nullable": true
          },
          "longitude": {
            "type": "number",
            "description": "Longitude.",
            "nullable": true
          },
          "description": {
            "type": "string",
            "description": "Short description for the card.",
            "nullable": true
          },
          "image_url": {
            "type": "string",
            "description": "Primary photo.",
            "format": "uri",
            "nullable": true
          }
        }
      },
      "Room": {
        "type": "object",
        "description": "The room and board this offer is for. Part of the hotel family block (spec §9.5).",
        "required": [
          "name"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Your room-type identifier."
          },
          "name": {
            "type": "string",
            "description": "Room name, e.g. 'Double standard'."
          },
          "board": {
            "type": "string",
            "description": "Board basis, e.g. 'Bed and breakfast'.",
            "nullable": true
          }
        }
      },
      "Rate": {
        "type": "object",
        "description": "The rate this offer is priced on. Part of the hotel family block (spec §9.5).",
        "properties": {
          "rate_type": {
            "type": "string",
            "description": "Your rate type.",
            "nullable": true
          },
          "rate_class": {
            "type": "string",
            "description": "Your rate class.",
            "nullable": true
          },
          "cancellation_policies": {
            "type": "array",
            "description": "Free-form policy objects, published to the buyer as sent.",
            "items": {
              "type": "object"
            }
          }
        }
      },
      "Stay": {
        "type": "object",
        "description": "The nights this offer is priced for. Part of the hotel family block (spec §9.5). A stay is [check_in, check_out) — the departure day is not a night.",
        "required": [
          "check_in",
          "check_out"
        ],
        "properties": {
          "check_in": {
            "type": "string",
            "description": "First night.",
            "format": "date"
          },
          "check_out": {
            "type": "string",
            "description": "Departure day.",
            "format": "date"
          },
          "adults": {
            "type": "integer",
            "description": "Adults the price covers.",
            "nullable": true
          },
          "children": {
            "type": "integer",
            "description": "Children the price covers.",
            "nullable": true
          },
          "rooms": {
            "type": "integer",
            "description": "Rooms the price covers.",
            "nullable": true
          }
        }
      },
      "Flight": {
        "type": "object",
        "description": "The travel family block (spec §9.6): one priced itinerary. A merchant-hosted travel supplier sells a fixed, all-in fare — there are no seat maps, bags or other ancillaries in this spec.",
        "required": [
          "origin_airport",
          "destination_airport",
          "legs"
        ],
        "properties": {
          "airline": {
            "type": "string",
            "description": "Marketing carrier name.",
            "nullable": true
          },
          "origin_airport": {
            "type": "string",
            "description": "Three-letter IATA code."
          },
          "destination_airport": {
            "type": "string",
            "description": "Three-letter IATA code."
          },
          "legs": {
            "type": "array",
            "description": "Human-readable segments, in order.",
            "items": {
              "type": "string"
            }
          },
          "departure_time": {
            "$ref": "#/components/schemas/OffsetDateTime"
          },
          "arrival_time": {
            "$ref": "#/components/schemas/OffsetDateTime"
          },
          "duration": {
            "type": "string",
            "description": "Total duration, e.g. '3h 0m'.",
            "nullable": true
          },
          "price_per_person": {
            "type": "string",
            "description": "Decimal string, per passenger.",
            "nullable": true
          },
          "fare_code": {
            "type": "string",
            "description": "Omit entirely rather than sending null."
          },
          "fare_brand": {
            "type": "string",
            "description": "Omit entirely rather than sending null."
          },
          "fare_includes": {
            "type": "array",
            "description": "What the fare covers. Omit entirely rather than sending null.",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "Dimensions": {
        "type": "object",
        "description": "Parcel dimensions in centimetres. Part of the shipping family block (spec §9.7).",
        "properties": {
          "w": {
            "type": "number",
            "description": "Width, cm."
          },
          "h": {
            "type": "number",
            "description": "Height, cm."
          },
          "l": {
            "type": "number",
            "description": "Length, cm."
          }
        }
      },
      "Shipment": {
        "type": "object",
        "description": "The shipping family block (spec §9.7): the parcel this quote is for. Echo the route and weight you actually priced — a buyer comparing two quotes needs to know they are for the same parcel.",
        "required": [
          "origin",
          "destination",
          "weight_kg"
        ],
        "properties": {
          "origin": {
            "type": "string",
            "description": "Collection country, ISO-3166-1 alpha-2."
          },
          "destination": {
            "type": "string",
            "description": "Delivery country, ISO-3166-1 alpha-2."
          },
          "weight_kg": {
            "type": "string",
            "description": "Decimal string, kilograms, above zero."
          },
          "transportation_type": {
            "type": "string",
            "description": "Your service mode, e.g. 'locker_locker'.",
            "nullable": true
          },
          "package_type": {
            "type": "string",
            "description": "Your package type, e.g. 'parcel'.",
            "nullable": true
          },
          "carrier": {
            "type": "string",
            "description": "Carrier name.",
            "nullable": true
          },
          "service_name": {
            "type": "string",
            "description": "Service name shown to the buyer.",
            "nullable": true
          },
          "transit_days": {
            "type": "integer",
            "description": "Expected transit days.",
            "nullable": true
          },
          "dimensions": {
            "$ref": "#/components/schemas/Dimensions"
          }
        }
      },
      "SalonLikeOffer": {
        "description": "An offer returned for a salon-like category (spec §9.1). Carries `slots` and the master/service/partner block.",
        "allOf": [
          {
            "$ref": "#/components/schemas/Offer"
          },
          {
            "type": "object",
            "required": [
              "slots"
            ]
          }
        ]
      },
      "CarRentalOffer": {
        "description": "An offer returned for `category=car_rental` (spec §9.4). `price` is the DAILY rate, which is why `price_unit` is required here. Carries no `slots`: the rental window is agreed at book, in `extra.date_from` / `extra.date_to`.",
        "allOf": [
          {
            "$ref": "#/components/schemas/Offer"
          },
          {
            "type": "object",
            "required": [
              "price_unit",
              "vehicle"
            ],
            "properties": {
              "price_unit": {
                "type": "string",
                "enum": [
                  "per_day"
                ]
              }
            }
          }
        ]
      },
      "HotelOffer": {
        "description": "An offer returned for `category=hotel` (spec §9.5): one room, on one rate, for the stay that was searched. Carries no `slots`.",
        "allOf": [
          {
            "$ref": "#/components/schemas/Offer"
          },
          {
            "type": "object",
            "required": [
              "hotel",
              "room"
            ]
          }
        ]
      },
      "TravelOffer": {
        "description": "An offer returned for `category=travel` (spec §9.6): one priced itinerary, bounded by `expires_at`. Carries no `slots`.",
        "allOf": [
          {
            "$ref": "#/components/schemas/Offer"
          },
          {
            "type": "object",
            "required": [
              "flight"
            ]
          }
        ]
      },
      "ShippingOffer": {
        "description": "An offer returned for `category=shipping` (spec §9.7): a quote for the parcel described in the search. Carries no `slots`.",
        "allOf": [
          {
            "$ref": "#/components/schemas/Offer"
          },
          {
            "type": "object",
            "required": [
              "shipment"
            ]
          }
        ]
      }
    }
  }
}
