{
  "openapi": "3.0.3",
  "info": {
    "title": "TryYourSize API",
    "version": "0.1.0",
    "description": "Server API conventions: authenticate with `Authorization: Bearer sk_…` (or a restricted `rk_…` key holding the operation's key scopes). Pin the version with the `TYS-Version` header (a date, e.g. `2026-10-25`; without it the latest version answers and the response's `TYS-Version` says which). Errors are `{error, status, code, doc_url, request_id}`. POSTs accept an `Idempotency-Key` (responses stored 24 h). Lists are cursor-paginated (`limit`, `cursor` → `data`, `has_more`, `next_cursor`). Rate limits answer `429` with `Retry-After`; every response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`.",
    "contact": {
      "name": "TryYourSize developers",
      "url": "https://docs.tryyoursize.com/"
    }
  },
  "servers": [
    {
      "url": "https://api.tryyoursize.com",
      "description": "Production (test and live mode are chosen by the key)"
    }
  ],
  "security": [
    {
      "PublishableKey": []
    }
  ],
  "tags": [
    {
      "name": "Embed",
      "description": "Called by TryYourSize.js and the measuring window from your page (publishable key). You do not call these yourself; they are listed so you can read your request logs and CSP reports."
    },
    {
      "name": "TryOn",
      "description": "The try-on window's routes (publishable key, from our window's origin). Shown for reference."
    },
    {
      "name": "Audit",
      "description": "Your organization's audit log, read with a restricted key holding `audit:read`."
    },
    {
      "name": "Outcomes",
      "description": "Orders and returns from your server, for the A/B results and size-related return rates. No customer data is accepted."
    },
    {
      "name": "Recommendations",
      "description": "The shopper's recommended size for your server or a headless storefront, from a handle the browser SDK issues. Sizes only, never measurements."
    },
    {
      "name": "Catalog API",
      "description": "Products, size maps, fit groups and garment drafts, with a secret key or a restricted key holding the listed scopes. The catalog is shared by test and live mode: test answers drafts, live answers published versions."
    }
  ],
  "paths": {
    "/v1/embed/init": {
      "post": {
        "operationId": "initEmbed",
        "summary": "Bootstrap the measurement window",
        "description": "**Key:** publishable (`pk_test_…` / `pk_live_…`), from an origin registered for the key.\n\nCalled when the measurement window opens. Checks the publishable key and that the brand page embedding the window (parent_origin) is allowed for the store.",
        "tags": [
          "Embed"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InitEmbedRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The store the window works for",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmbedStore"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        },
        "security": [
          {
            "PublishableKey": []
          }
        ]
      }
    },
    "/v1/products/{product}/tryon": {
      "parameters": [
        {
          "name": "product",
          "in": "path",
          "required": true,
          "description": "The product's SKU / style ID in the store.",
          "schema": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120
          }
        }
      ],
      "get": {
        "operationId": "getProductTryOn",
        "summary": "Is \"See it on you\" available for a product?",
        "description": "**Key:** publishable (`pk_test_…` / `pk_live_…`), from an origin registered for the key.\n\nUsed by the inline element, the size-guide button and the window to decide whether to offer the try-on. Called from the brand's page (an allowed origin of the store) or from the window. `sizes` are the garment's sizes as entered in Studio; `shop_sizes` are the store's own labels for them (the product's size map, e.g. \"M\" → \"Medium\"), so the page can preselect its own variant.",
        "tags": [
          "TryOn"
        ],
        "responses": {
          "200": {
            "description": "Availability (never 404 for an unknown product, `available` is false)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProductTryOn"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        },
        "security": [
          {
            "PublishableKey": []
          }
        ]
      }
    },
    "/v1/tryon": {
      "post": {
        "operationId": "startTryOn",
        "summary": "See a product on the shopper's body",
        "description": "**Key:** publishable (`pk_test_…` / `pk_live_…`), from an origin registered for the key.\n\nShows every size of the product's published garment on the body and recommends a size (from a few seconds to about 2 minutes). A body already shown with this garment version answers 200 at once; otherwise 202 and a job to poll. Every answer carries a new `viewer_token`, the only way (with the shopper's session) to read the job. With a shopper session (`Authorization: Bearer tys_sess_…`) the try-on is linked to the shopper and, in live mode, delivering a done result records the store connection and the size the store receives.",
        "tags": [
          "TryOn"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/tryon_view"
          },
          {
            "name": "start",
            "in": "query",
            "required": false,
            "description": "`false` answers only with an existing result and never queues a simulation: a done or running try-on of this body, or 404 (\"not simulated yet\"). With `view=summary`, a miss may answer 200 with a size estimate instead (`status: done`, empty `id`, `summary.method: estimate`). Page views (the size-guide and inline elements) use it; a full simulation starts only on an explicit shopper action (\"See it on you\").",
            "schema": {
              "type": "boolean",
              "default": true
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TryOnRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done (an existing result)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TryOnJob"
                }
              }
            }
          },
          "202": {
            "description": "Queued or running; poll GET /v1/tryon/{tryon_id}",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TryOnJob"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No published garment for the product, or (start=false) this body was not simulated yet",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        },
        "security": [
          {
            "PublishableKey": []
          }
        ]
      }
    },
    "/v1/tryon/{tryon_id}": {
      "parameters": [
        {
          "name": "tryon_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "get": {
        "operationId": "getTryOn",
        "summary": "A try-on's status, progress and result",
        "description": "**Key:** publishable (`pk_test_…` / `pk_live_…`), from an origin registered for the key.\n\nReadable only by the viewer the try-on was issued to: send the `viewer_token` from POST /v1/tryon as `Authorization: Bearer tys_view_…`, or the session of the signed-in shopper who started it. Anyone else (another session, no token) gets 404.",
        "tags": [
          "TryOn"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/tryon_view"
          }
        ],
        "responses": {
          "200": {
            "description": "The try-on",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TryOnJob"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "PublishableKey": []
          }
        ]
      }
    },
    "/v1/products/{product}/appearance": {
      "parameters": [
        {
          "name": "product",
          "in": "path",
          "required": true,
          "description": "The product's SKU / style ID in the store.",
          "schema": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120
          }
        },
        {
          "name": "colour",
          "in": "query",
          "required": false,
          "description": "The shop's selected colour option value; picks `selected` (case-insensitive key match).",
          "schema": {
            "type": "string",
            "maxLength": 80
          }
        }
      ],
      "get": {
        "operationId": "getPublicProductAppearance",
        "summary": "The colourways the window draws on the simulated garment",
        "description": "**Key:** publishable (`pk_test_…` / `pk_live_…`), from an origin registered for the key.\n\nCalled by the window (or the brand's page) with the publishable key, from an allowed origin of the store or the window's own domain. Never 404: an unknown product, or one without appearance, has no colourways. Cached by the browser for a minute.",
        "tags": [
          "TryOn"
        ],
        "responses": {
          "200": {
            "description": "Appearance",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicProductAppearance"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        },
        "security": [
          {
            "PublishableKey": []
          }
        ]
      }
    },
    "/v1/audit_events": {
      "get": {
        "operationId": "listAuditEventsWithKey",
        "summary": "Audit log of the key's organization (server API)",
        "tags": [
          "Audit"
        ],
        "description": "**Key scopes:** `audit:read`. A secret key (`sk_`) has every scope; a restricted key (`rk_`) needs the scopes listed.\n\nThe audit log of the secret key's organization, newest first. Same filters as the Studio listing (no CSV). Secret keys (sk_) and restricted keys (rk_) with the `audit:read` scope.",
        "x-key-scopes": [
          "audit:read"
        ],
        "security": [
          {
            "SecretKey": []
          }
        ],
        "parameters": [
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "action",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "actor",
            "in": "query",
            "required": false,
            "description": "actor_type:actor_id",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "store_id",
            "in": "query",
            "required": false,
            "description": "Only events of this store",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "target",
            "in": "query",
            "required": false,
            "description": "target_type or target_type:target_id, e.g. api_key:<id>",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Events at or after this time",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Events before this time",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size (default 50, at most 200; ignored by the CSV export)",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuditEventPage"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/orders": {
      "post": {
        "operationId": "createOutcomeOrder",
        "summary": "Record an order (idempotent by order ID)",
        "tags": [
          "Outcomes"
        ],
        "description": "**Key scopes:** `orders:write`. A secret key (`sk_`) has every scope; a restricted key (`rk_`) needs the scopes listed.\n\nFrom the brand's server with a secret key or a restricted key with `orders:write`. Re-sending the same `id` updates the order (200); a new one is 201. The A/B arm comes from `visitor_id` (recomputed by TryYourSize) or `experiment_arm`. No customer data is accepted.",
        "x-key-scopes": [
          "orders:write"
        ],
        "security": [
          {
            "SecretKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OutcomeOrderInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OutcomeOrderResult"
                }
              }
            }
          },
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OutcomeOrderResult"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/v1/returns": {
      "post": {
        "operationId": "createOutcomeReturn",
        "summary": "Record a return of an order line (idempotent by return ID and line)",
        "tags": [
          "Outcomes"
        ],
        "description": "**Key scopes:** `returns:write`. A secret key (`sk_`) has every scope; a restricted key (`rk_`) needs the scopes listed.\n\nSecret key, or a restricted key with `returns:write` (e.g. a returns platform). The order must have been sent first (404 otherwise). Reasons are normalised to TryYourSize's taxonomy.",
        "x-key-scopes": [
          "returns:write"
        ],
        "security": [
          {
            "SecretKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OutcomeReturnInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OutcomeReturnResult"
                }
              }
            }
          },
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OutcomeReturnResult"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/funnel_events": {
      "post": {
        "operationId": "recordFunnelEvent",
        "summary": "Count a shopper funnel step (measuring window)",
        "tags": [
          "Embed"
        ],
        "description": "**Key:** publishable (`pk_test_…` / `pk_live_…`), from an origin registered for the key.\n\nThe measuring window reports the one funnel step no other request shows: the shopper chose a size (\"Use this size\"). The Platform counts it server-side, cookieless and aggregated: no identifier is sent or stored, no person profile is made; the event carries only the store, the product category, the language, the device class and the surface. The product is used to find its category and is never stored. Always 204 (also when analytics are off).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FunnelEvent"
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Counted (or dropped when analytics are off)"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        },
        "security": [
          {
            "PublishableKey": []
          }
        ]
      }
    },
    "/v1/recommendation_handles": {
      "post": {
        "operationId": "createRecommendationHandle",
        "summary": "Seal the signed-in shopper's body into a handle for the brand's server",
        "tags": [
          "Recommendations"
        ],
        "description": "**Key:** publishable (`pk_test_…` / `pk_live_…`), from an origin registered for the key.\n\nCalled by the TryYourSize window (the SDK's invisible status frame), never by a brand's code: it needs the shopper's session (`Authorization: Bearer tys_sess_…`), which only our origin can read, plus the store's publishable key. The answer is an encrypted handle bound to the store, the mode and the shopper; the page passes it to the brand's server for POST /v1/recommendations. Nothing is stored: the handle itself is the sealed body, and neither the page nor the brand's server can read it. Expires after an hour.",
        "security": [
          {
            "PublishableKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RecommendationHandleRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecommendationHandle"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          }
        }
      }
    },
    "/v1/recommendations": {
      "post": {
        "operationId": "createRecommendations",
        "summary": "The shopper's recommended size for up to 25 products",
        "tags": [
          "Recommendations"
        ],
        "description": "**Key scopes:** `fit_checks:read`. A secret key (`sk_`) has every scope; a restricted key (`rk_`) needs the scopes listed.\n\nFrom the brand's server (or a headless storefront's backend) with a secret key, or a restricted key with `fit_checks:read`. Send the handle the browser SDK gave your page (`tys.recommendationHandle()`); the answer has sizes only, in your size labels, never measurements. Like a page view, it never starts a simulation: a body already simulated with the garment answers with the existing result, or with a size estimate when one is certain for this body; otherwise the status is `unavailable` until the shopper asks for the try-on. A delivered size counts as a fit check, like in the browser. The handle's mode must match the key's (a test handle with a live key is 403). A handle whose shopper deleted their account or disconnected the store is refused (401 `handle_invalid`).",
        "x-key-scopes": [
          "fit_checks:read"
        ],
        "security": [
          {
            "SecretKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RecommendationsRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "One answer per product, in the order asked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecommendationList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/v1/products": {
      "get": {
        "operationId": "listCatalogProducts",
        "summary": "List the store's products",
        "tags": [
          "Catalog API"
        ],
        "description": "**Key scopes:** `products:read`. A secret key (`sk_`) has every scope; a restricted key (`rk_`) needs the scopes listed.\n\nNewest first, cursor-paginated. `sku` finds one product by its SKU / style ID.",
        "x-key-scopes": [
          "products:read"
        ],
        "security": [
          {
            "SecretKey": []
          }
        ],
        "parameters": [
          {
            "name": "sku",
            "in": "query",
            "required": false,
            "description": "Only the product with this SKU / style ID.",
            "schema": {
              "type": "string",
              "maxLength": 120
            }
          },
          {
            "$ref": "#/components/parameters/ListLimit"
          },
          {
            "$ref": "#/components/parameters/ListCursor"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProductPage"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      },
      "post": {
        "operationId": "createCatalogProduct",
        "summary": "Create a product",
        "tags": [
          "Catalog API"
        ],
        "description": "**Key scopes:** `products:write`. A secret key (`sk_`) has every scope; a restricted key (`rk_`) needs the scopes listed.\n\nThe SKU / style ID (`external_id`) is unique per store (409). Send an `Idempotency-Key` to retry safely.",
        "x-key-scopes": [
          "products:write"
        ],
        "security": [
          {
            "SecretKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProductInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Product"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      }
    },
    "/v1/products/{product}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProductRef"
        }
      ],
      "get": {
        "operationId": "getCatalogProduct",
        "summary": "A product",
        "tags": [
          "Catalog API"
        ],
        "description": "**Key scopes:** `products:read`. A secret key (`sk_`) has every scope; a restricted key (`rk_`) needs the scopes listed.\n\nBy ID or SKU.",
        "x-key-scopes": [
          "products:read"
        ],
        "security": [
          {
            "SecretKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Product"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "patch": {
        "operationId": "updateCatalogProduct",
        "summary": "Update a product",
        "tags": [
          "Catalog API"
        ],
        "description": "**Key scopes:** `products:write`. A secret key (`sk_`) has every scope; a restricted key (`rk_`) needs the scopes listed.\n\nReplaces the SKU, name and category (all three are required, like Studio).",
        "x-key-scopes": [
          "products:write"
        ],
        "security": [
          {
            "SecretKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProductInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Product"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      }
    },
    "/v1/products/{product}/size_map": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProductRef"
        }
      ],
      "get": {
        "operationId": "getCatalogSizeMap",
        "summary": "A product's size map (Studio size → your size label)",
        "tags": [
          "Catalog API"
        ],
        "description": "**Key scopes:** `products:read`. A secret key (`sk_`) has every scope; a restricted key (`rk_`) needs the scopes listed.",
        "x-key-scopes": [
          "products:read"
        ],
        "security": [
          {
            "SecretKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProductSizeMap"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "put": {
        "operationId": "saveCatalogSizeMap",
        "summary": "Replace a product's size map",
        "tags": [
          "Catalog API"
        ],
        "description": "**Key scopes:** `products:write`. A secret key (`sk_`) has every scope; a restricted key (`rk_`) needs the scopes listed.\n\nRecommendations are returned in your labels through this map (\"M\" → \"Medium\"). Publishing needs every shop size mapped.",
        "x-key-scopes": [
          "products:write"
        ],
        "security": [
          {
            "SecretKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProductSizeMap"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProductSizeMap"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "Values to fix (each issue points at the field, size and POM)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GarmentRejected"
                }
              }
            }
          }
        }
      }
    },
    "/v1/products/{product}/garments": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProductRef"
        }
      ],
      "get": {
        "operationId": "listCatalogGarments",
        "summary": "Garment versions of a product, newest first",
        "tags": [
          "Catalog API"
        ],
        "description": "**Key scopes:** `products:read`. A secret key (`sk_`) has every scope; a restricted key (`rk_`) needs the scopes listed.\n\n`has_more` is always false (a product has few versions).",
        "x-key-scopes": [
          "products:read"
        ],
        "security": [
          {
            "SecretKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GarmentVersionPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "post": {
        "operationId": "createCatalogGarmentDraft",
        "summary": "Start a new draft garment version",
        "tags": [
          "Catalog API"
        ],
        "description": "**Key scopes:** `garments:write`. A secret key (`sk_`) has every scope; a restricted key (`rk_`) needs the scopes listed.\n\nSizes, points of measure (mm) per size, grading and fabric, as in Studio. A product has at most one draft (409). Send an `Idempotency-Key` to retry safely.",
        "x-key-scopes": [
          "garments:write"
        ],
        "security": [
          {
            "SecretKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GarmentInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Draft created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GarmentVersion"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "description": "Values to fix (each issue points at the field, size and POM)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GarmentRejected"
                }
              }
            }
          }
        }
      }
    },
    "/v1/garments/{garment_id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/GarmentID"
        }
      ],
      "get": {
        "operationId": "getCatalogGarment",
        "summary": "A garment version",
        "tags": [
          "Catalog API"
        ],
        "description": "**Key scopes:** `products:read`. A secret key (`sk_`) has every scope; a restricted key (`rk_`) needs the scopes listed.",
        "x-key-scopes": [
          "products:read"
        ],
        "security": [
          {
            "SecretKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GarmentVersion"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "put": {
        "operationId": "updateCatalogGarmentDraft",
        "summary": "Replace a draft's content",
        "tags": [
          "Catalog API"
        ],
        "description": "**Key scopes:** `garments:write`. A secret key (`sk_`) has every scope; a restricted key (`rk_`) needs the scopes listed.\n\nDrafts may be incomplete but never hold impossible values. Published versions never change (409).",
        "x-key-scopes": [
          "garments:write"
        ],
        "security": [
          {
            "SecretKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GarmentInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GarmentVersion"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "description": "Values to fix (each issue points at the field, size and POM)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GarmentRejected"
                }
              }
            }
          }
        }
      }
    },
    "/v1/garments/{garment_id}/publish": {
      "parameters": [
        {
          "$ref": "#/components/parameters/GarmentID"
        }
      ],
      "post": {
        "operationId": "publishCatalogGarment",
        "summary": "Publish a complete draft",
        "tags": [
          "Catalog API"
        ],
        "description": "**Key scopes:** `garments:publish`. A secret key (`sk_`) has every scope; a restricted key (`rk_`) needs the scopes listed.\n\nThe version shoppers are fitted against from now on, for every product of its fit group. The same checks as Studio: every required point of measure for every size, fabric, the fit approval when the store requires it, every shop size mapped, and the plan's styles (402). Test answers drafts, live answers published versions only.",
        "x-key-scopes": [
          "garments:publish"
        ],
        "security": [
          {
            "SecretKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "responses": {
          "200": {
            "description": "Published",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GarmentVersion"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "description": "Values to fix (each issue points at the field, size and POM)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GarmentRejected"
                }
              }
            }
          }
        }
      }
    },
    "/v1/fit_groups": {
      "get": {
        "operationId": "listCatalogFitGroups",
        "summary": "List fit groups",
        "tags": [
          "Catalog API"
        ],
        "description": "**Key scopes:** `products:read`. A secret key (`sk_`) has every scope; a restricted key (`rk_`) needs the scopes listed.\n\nCursor-paginated.",
        "x-key-scopes": [
          "products:read"
        ],
        "security": [
          {
            "SecretKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ListLimit"
          },
          {
            "$ref": "#/components/parameters/ListCursor"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FitGroupPage"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      },
      "post": {
        "operationId": "createCatalogFitGroup",
        "summary": "Create a fit group",
        "tags": [
          "Catalog API"
        ],
        "description": "**Key scopes:** `products:write`. A secret key (`sk_`) has every scope; a restricted key (`rk_`) needs the scopes listed.\n\nOne garment shared by several products (colourways, prints, reorders). Send an `Idempotency-Key` to retry safely.",
        "x-key-scopes": [
          "products:write"
        ],
        "security": [
          {
            "SecretKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FitGroupInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FitGroup"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      }
    },
    "/v1/fit_groups/{fit_group_id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/FitGroupRef"
        }
      ],
      "get": {
        "operationId": "getCatalogFitGroup",
        "summary": "A fit group",
        "tags": [
          "Catalog API"
        ],
        "description": "**Key scopes:** `products:read`. A secret key (`sk_`) has every scope; a restricted key (`rk_`) needs the scopes listed.",
        "x-key-scopes": [
          "products:read"
        ],
        "security": [
          {
            "SecretKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FitGroup"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/fit_groups/{fit_group_id}/products": {
      "parameters": [
        {
          "$ref": "#/components/parameters/FitGroupRef"
        }
      ],
      "post": {
        "operationId": "linkCatalogFitGroupProducts",
        "summary": "Move products into a fit group",
        "tags": [
          "Catalog API"
        ],
        "description": "**Key scopes:** `products:write`. A secret key (`sk_`) has every scope; a restricted key (`rk_`) needs the scopes listed.\n\nThe products then use this group's garment.",
        "x-key-scopes": [
          "products:write"
        ],
        "security": [
          {
            "SecretKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LinkProductsRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FitGroup"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "FitGroupRef": {
        "name": "fit_group_id",
        "in": "path",
        "required": true,
        "description": "A fit group.",
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      },
      "GarmentID": {
        "name": "garment_id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "format": "uuid"
        },
        "description": "A garment *version* (garment_versions.id). A fit group (garments table) is `fit_group_id`."
      },
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "A unique value per logical request (a UUID v4 is ideal), 1-255 printable ASCII characters. With a secret or restricted key, the first response is stored for 24 hours per store and mode: a retry with the same key and the same body gets the stored response back (header `Idempotent-Replayed: true`); the same key with a different body is `422 idempotency_key_reused`; a retry while the first request still runs is `409 idempotency_key_in_use`. Responses with a 5xx status are not stored, so a retry runs again.",
        "schema": {
          "type": "string",
          "minLength": 1,
          "maxLength": 255
        }
      },
      "ListCursor": {
        "name": "cursor",
        "in": "query",
        "required": false,
        "description": "Opaque cursor from the previous page's `next_cursor` (list convention). Omit it for the first page.",
        "schema": {
          "type": "string",
          "maxLength": 512
        }
      },
      "ListLimit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "description": "Page size (list convention). Default 20, at most 100.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 20
        }
      },
      "ProductRef": {
        "name": "product",
        "in": "path",
        "required": true,
        "description": "The product: its TryYourSize ID (UUID) or the brand's SKU / style ID (`external_id`). An ID is looked up first.",
        "schema": {
          "type": "string",
          "minLength": 1,
          "maxLength": 120
        }
      },
      "tryon_view": {
        "name": "view",
        "in": "query",
        "required": false,
        "description": "`full` (default) answers with the whole `result` (meshes for every size, MBs). `summary` leaves `result` out and answers `summary` instead (the recommended size and the sizes that can be worn, a few bytes): for the size-guide button and the store's recommended-size event, which never need the meshes.",
        "schema": {
          "type": "string",
          "enum": [
            "full",
            "summary"
          ],
          "default": "full"
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Invalid request",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Conflict": {
        "description": "Conflicts with the current state (duplicate SKU, editing a published version)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "Not allowed",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "Not found",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "ServiceUnavailable": {
        "description": "A dependency is temporarily unavailable; retry later",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "Too many requests; retry later",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing or invalid credentials",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unprocessable": {
        "description": "The content is rejected (e.g. a body that cannot be rebuilt)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "AppearanceAsset": {
        "type": "object",
        "description": "Print artwork uploaded by the brand: a repeat tile or a placement print (PNG, JPEG or SVG). Immutable. The same file uploaded twice to a store is the same asset.",
        "required": [
          "id",
          "filename",
          "content_type",
          "bytes",
          "width_px",
          "height_px",
          "url",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "filename": {
            "type": "string"
          },
          "content_type": {
            "type": "string",
            "enum": [
              "image/png",
              "image/jpeg",
              "image/svg+xml"
            ]
          },
          "bytes": {
            "type": "integer",
            "format": "int64"
          },
          "width_px": {
            "type": "integer",
            "description": "Pixel width (SVG, its viewBox width)."
          },
          "height_px": {
            "type": "integer"
          },
          "url": {
            "type": "string",
            "description": "Path of the file on this API (`/v1/appearance_assets/{id}`), relative to the API base URL."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "AppearanceColourway": {
        "type": "object",
        "description": "One colour of a product. A product that is itself a colourway (colourways are separate products in one fit group) has one, with an empty `key`. A product that carries several colours as variants (a Shopify product with a Color option) has one per option value, keyed by that value. The first is the default.",
        "required": [
          "key",
          "base_color",
          "finish"
        ],
        "properties": {
          "key": {
            "type": "string",
            "maxLength": 80,
            "description": "The shop's colour option value (\"Rinse indigo\"), or \"\" for the product itself."
          },
          "name": {
            "type": "string",
            "maxLength": 80,
            "description": "Display name of the colour."
          },
          "base_color": {
            "type": "string",
            "pattern": "^#[0-9a-fA-F]{6}$",
            "description": "The fabric's colour, sRGB hex."
          },
          "finish": {
            "$ref": "#/components/schemas/AppearanceFinish"
          },
          "print": {
            "$ref": "#/components/schemas/AppearanceRepeatPrint"
          },
          "panels": {
            "type": "array",
            "maxItems": 16,
            "items": {
              "$ref": "#/components/schemas/AppearancePanelOverride"
            }
          }
        }
      },
      "AppearanceFinish": {
        "type": "string",
        "description": "Fabric finish preset. The viewer draws its surface detail (normal and roughness) from small procedural maps at true scale: `matte_jersey` (knit loops, soft sheen), `denim_twill` (diagonal 2/1 twill), `rib` (wales along the grain), `satin` (smooth, low roughness, sheen).",
        "enum": [
          "matte_jersey",
          "denim_twill",
          "rib",
          "satin"
        ]
      },
      "AppearancePanelOverride": {
        "type": "object",
        "description": "Changes the print on one pattern piece. `panel` is the piece name, as in the try-on result's `panels` and the flat preview: `front`, `back`, `sleeve_pos`, `sleeve_neg` (T-shirt); `front_pos`, `back_pos`, `front_neg`, `back_neg`, `waistband` (jeans).",
        "required": [
          "panel"
        ],
        "properties": {
          "panel": {
            "type": "string",
            "pattern": "^[a-z0-9_]{1,40}$"
          },
          "hide_repeat": {
            "type": "boolean",
            "description": "Leave this piece in the base colour (no all-over print)."
          },
          "placement": {
            "$ref": "#/components/schemas/AppearancePlacementPrint"
          }
        }
      },
      "AppearancePlacementPrint": {
        "type": "object",
        "description": "Artwork printed once on a pattern piece (a chest print), positioned in cm.",
        "required": [
          "asset_id",
          "width_cm",
          "x_cm",
          "y_cm"
        ],
        "properties": {
          "asset_id": {
            "type": "string",
            "format": "uuid"
          },
          "width_cm": {
            "type": "number",
            "format": "double",
            "minimum": 0.5,
            "maximum": 150,
            "description": "Printed width, cm. The height follows the artwork's aspect ratio."
          },
          "x_cm": {
            "type": "number",
            "format": "double",
            "description": "Centre of the artwork from the piece's centre line, cm (+ toward the piece's +u side)."
          },
          "y_cm": {
            "type": "number",
            "format": "double",
            "description": "Centre of the artwork down from the piece's highest point (the high point shoulder on a bodice), cm."
          },
          "rotation_deg": {
            "type": "number",
            "format": "double"
          }
        }
      },
      "AppearanceRepeatPrint": {
        "type": "object",
        "description": "An all-over print: one tile of the brand's artwork repeated at its real size, placed in each pattern piece's own frame (u across the grain, v along it), like printed fabric under the cutter. The print is continuous inside a piece and breaks at seams, as on a real garment.",
        "required": [
          "asset_id",
          "width_cm",
          "height_cm"
        ],
        "properties": {
          "asset_id": {
            "type": "string",
            "format": "uuid"
          },
          "width_cm": {
            "type": "number",
            "format": "double",
            "minimum": 0.2,
            "maximum": 200,
            "description": "Width of one repeat on the fabric, cm (across the grain before rotation)."
          },
          "height_cm": {
            "type": "number",
            "format": "double",
            "minimum": 0.2,
            "maximum": 200,
            "description": "Height of one repeat on the fabric, cm (along the grain before rotation)."
          },
          "offset_x_cm": {
            "type": "number",
            "format": "double",
            "description": "Shift of the tile origin across the grain, cm."
          },
          "offset_y_cm": {
            "type": "number",
            "format": "double",
            "description": "Shift of the tile origin along the grain, cm."
          },
          "rotation_deg": {
            "type": "number",
            "format": "double",
            "description": "Angle of the tile's vertical to the grainline, degrees counter-clockwise (0 = upright on the grain)."
          }
        }
      },
      "AuditEvent": {
        "description": "One append-only audit row. Never secrets or shopper data.",
        "type": "object",
        "required": [
          "id",
          "actor_type",
          "actor_id",
          "action",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "organization_id": {
            "type": "string",
            "format": "uuid"
          },
          "store_id": {
            "type": "string",
            "format": "uuid"
          },
          "actor_type": {
            "type": "string",
            "enum": [
              "user",
              "api_key",
              "partner_user",
              "staff",
              "system",
              "shopify"
            ]
          },
          "actor_id": {
            "type": "string"
          },
          "actor_label": {
            "type": "string"
          },
          "on_behalf_of": {
            "type": "string",
            "format": "uuid"
          },
          "action": {
            "type": "string"
          },
          "target_type": {
            "type": "string"
          },
          "target_id": {
            "type": "string"
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true
          },
          "ip": {
            "type": "string"
          },
          "user_agent": {
            "type": "string"
          },
          "request_id": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "AuditEventPage": {
        "type": "object",
        "required": [
          "events"
        ],
        "properties": {
          "events": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AuditEvent"
            }
          },
          "next_cursor": {
            "type": "string"
          }
        }
      },
      "BodyBasics": {
        "type": "object",
        "required": [
          "sex",
          "age_years",
          "height_mm",
          "weight_kg"
        ],
        "properties": {
          "sex": {
            "type": "string",
            "enum": [
              "female",
              "male"
            ]
          },
          "age_years": {
            "type": "integer",
            "minimum": 1
          },
          "height_mm": {
            "type": "number",
            "format": "double",
            "exclusiveMinimum": true,
            "minimum": 0
          },
          "weight_kg": {
            "type": "number",
            "format": "double",
            "exclusiveMinimum": true,
            "minimum": 0
          }
        }
      },
      "EmbedStore": {
        "type": "object",
        "required": [
          "store_name",
          "mode"
        ],
        "properties": {
          "store_name": {
            "type": "string"
          },
          "mode": {
            "type": "string",
            "enum": [
              "test",
              "live"
            ]
          }
        }
      },
      "Error": {
        "type": "object",
        "description": "Every error has this shape. `error` is a human-readable message, `status` the HTTP status. `code` is stable and machine-readable; `doc_url` links its explanation; `request_id` identifies the request (also in the `X-Request-ID` response header): quote it to support.",
        "required": [
          "error",
          "status"
        ],
        "properties": {
          "error": {
            "type": "string"
          },
          "status": {
            "type": "integer"
          },
          "code": {
            "type": "string",
            "description": "Stable error code, e.g. invalid_request, unauthorized, reauth_required, payment_required, forbidden, not_found, conflict, unprocessable, rate_limited, internal_error, not_implemented, unavailable, invalid_api_version, idempotency_key_in_use, idempotency_key_reused, invalid_idempotency_key, handle_expired, handle_invalid.",
            "example": "not_found"
          },
          "doc_url": {
            "type": "string",
            "format": "uri",
            "example": "https://docs.tryyoursize.com/api/errors/#not_found"
          },
          "request_id": {
            "type": "string"
          }
        }
      },
      "Fabric": {
        "type": "object",
        "description": "What the tech pack / mill sheet says.",
        "required": [
          "composition"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "construction": {
            "type": "string",
            "enum": [
              "jersey",
              "rib",
              "interlock",
              "fleece",
              "pique",
              "plain_weave",
              "twill",
              "denim",
              "satin",
              "other"
            ]
          },
          "composition": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FiberShare"
            }
          },
          "weight_gsm": {
            "type": "number",
            "format": "double",
            "description": "Areal density, g/m²."
          },
          "stretch_warp_pct": {
            "type": "number",
            "format": "double",
            "description": "Stretch along the warp (lengthwise), percent."
          },
          "stretch_weft_pct": {
            "type": "number",
            "format": "double",
            "description": "Stretch along the weft (crosswise), percent."
          },
          "notes": {
            "type": "string"
          }
        }
      },
      "FiberShare": {
        "type": "object",
        "required": [
          "fiber",
          "pct"
        ],
        "properties": {
          "fiber": {
            "type": "string",
            "enum": [
              "cotton",
              "polyester",
              "elastane",
              "viscose",
              "modal",
              "lyocell",
              "wool",
              "linen",
              "polyamide",
              "silk",
              "acrylic",
              "other"
            ]
          },
          "pct": {
            "type": "number",
            "format": "double"
          }
        }
      },
      "FitGroup": {
        "type": "object",
        "description": "One garment (fit block + fabric) shared by many products: colourways, prints, reorders. Stored in the `garments` table; its versions are the garment versions.",
        "required": [
          "id",
          "name",
          "product_ids",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "product_ids": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            }
          },
          "published_version_id": {
            "type": "string",
            "format": "uuid"
          },
          "draft_version_id": {
            "type": "string",
            "format": "uuid"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "FitGroupInput": {
        "type": "object",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "product_ids": {
            "type": "array",
            "description": "Products to put in the new group (create only).",
            "items": {
              "type": "string",
              "format": "uuid"
            }
          },
          "source_product_id": {
            "type": "string",
            "format": "uuid",
            "description": "\"Use the garment of…\": the new group takes over this product's garment versions (create only)."
          }
        }
      },
      "FitGroupPage": {
        "type": "object",
        "description": "One page of a list (convention). Pass `next_cursor` as `cursor` for the next page while `has_more` is true.",
        "required": [
          "data",
          "has_more"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FitGroup"
            }
          },
          "has_more": {
            "type": "boolean"
          },
          "next_cursor": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "FitIntent": {
        "type": "string",
        "description": "The designer's intended silhouette; sets the ease targets the fit judgement compares against.",
        "enum": [
          "skinny",
          "slim",
          "regular",
          "relaxed",
          "oversized"
        ]
      },
      "FunnelEvent": {
        "type": "object",
        "description": "A shopper funnel step reported by the measuring window. No identifier, body data or size: only which step.",
        "required": [
          "event"
        ],
        "properties": {
          "event": {
            "type": "string",
            "enum": [
              "size_used"
            ]
          },
          "product": {
            "type": "string",
            "maxLength": 255,
            "description": "The product (SKU) the size is for; only used to find its category, never stored."
          }
        }
      },
      "GarmentCategory": {
        "type": "string",
        "description": "The garment type; it sets which POMs are needed.",
        "enum": [
          "tshirt",
          "shirt",
          "sweater",
          "jacket",
          "dress",
          "skirt",
          "jeans",
          "trousers",
          "shorts",
          "leggings",
          "polo",
          "hoodie"
        ]
      },
      "GarmentInput": {
        "type": "object",
        "required": [
          "category",
          "fit_intent",
          "sizes",
          "poms",
          "fabric"
        ],
        "properties": {
          "category": {
            "$ref": "#/components/schemas/GarmentCategory"
          },
          "fit_intent": {
            "$ref": "#/components/schemas/FitIntent"
          },
          "sizes": {
            "type": "array",
            "description": "Size labels, smallest to largest.",
            "items": {
              "type": "string"
            }
          },
          "poms": {
            "$ref": "#/components/schemas/POMTable"
          },
          "grading": {
            "$ref": "#/components/schemas/GradingRule"
          },
          "fabric": {
            "$ref": "#/components/schemas/Fabric"
          },
          "import_id": {
            "type": "string",
            "format": "uuid",
            "description": "Pattern import linked to this version (omit to unlink)."
          },
          "season": {
            "type": "string",
            "maxLength": 40,
            "description": "Season tag, e.g. \"SS27\"."
          },
          "tolerances_mm": {
            "$ref": "#/components/schemas/TolerancesMM"
          },
          "spec_source": {
            "$ref": "#/components/schemas/SpecSource"
          }
        }
      },
      "GarmentIssue": {
        "type": "object",
        "required": [
          "severity",
          "code",
          "field",
          "message"
        ],
        "properties": {
          "severity": {
            "type": "string",
            "enum": [
              "error",
              "warning"
            ]
          },
          "code": {
            "type": "string",
            "description": "e.g. out_of_range, not_graded, missing_pom, composition_total"
          },
          "field": {
            "type": "string",
            "description": "category | fit_intent | sizes | poms | grading | import | fabric.<field>"
          },
          "size": {
            "type": "string"
          },
          "pom": {
            "type": "string"
          },
          "params": {
            "$ref": "#/components/schemas/IssueParams"
          },
          "message": {
            "type": "string",
            "description": "English text; clients in other languages can word it from code, size, pom and params."
          }
        }
      },
      "GarmentRejected": {
        "type": "object",
        "required": [
          "error",
          "status",
          "issues"
        ],
        "properties": {
          "error": {
            "type": "string"
          },
          "status": {
            "type": "integer"
          },
          "issues": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GarmentIssue"
            }
          }
        }
      },
      "GarmentVersion": {
        "type": "object",
        "required": [
          "id",
          "product_id",
          "version",
          "status",
          "category",
          "fit_intent",
          "sizes",
          "poms",
          "fabric",
          "issues",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "product_id": {
            "type": "string",
            "format": "uuid"
          },
          "version": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "published",
              "retired"
            ]
          },
          "category": {
            "$ref": "#/components/schemas/GarmentCategory"
          },
          "fit_intent": {
            "$ref": "#/components/schemas/FitIntent"
          },
          "sizes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "poms": {
            "$ref": "#/components/schemas/POMTable"
          },
          "grading": {
            "$ref": "#/components/schemas/GradingRule"
          },
          "fabric": {
            "$ref": "#/components/schemas/Fabric"
          },
          "import_id": {
            "type": "string",
            "format": "uuid"
          },
          "issues": {
            "type": "array",
            "description": "What still blocks publishing (errors) or deserves a look (warnings).",
            "items": {
              "$ref": "#/components/schemas/GarmentIssue"
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "published_at": {
            "type": "string",
            "format": "date-time"
          },
          "season": {
            "type": "string"
          },
          "tolerances_mm": {
            "$ref": "#/components/schemas/TolerancesMM"
          },
          "spec_source": {
            "$ref": "#/components/schemas/SpecSource"
          },
          "fit_group_id": {
            "type": "string",
            "format": "uuid",
            "description": "The fit group (garments row) this version belongs to (absent until grouped)."
          }
        }
      },
      "GarmentVersionPage": {
        "type": "object",
        "description": "One page of a list (convention). Pass `next_cursor` as `cursor` for the next page while `has_more` is true.",
        "required": [
          "data",
          "has_more"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GarmentVersion"
            }
          },
          "has_more": {
            "type": "boolean"
          },
          "next_cursor": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "GradingRule": {
        "type": "object",
        "description": "How the POM table was derived: the base size's values plus, per POM, the increment (mm) between each pair of consecutive sizes (len = number of sizes - 1). The table stays the source of truth.",
        "required": [
          "base_size",
          "increments_mm"
        ],
        "properties": {
          "base_size": {
            "type": "string"
          },
          "increments_mm": {
            "type": "object",
            "additionalProperties": {
              "type": "array",
              "items": {
                "type": "number",
                "format": "double"
              }
            }
          }
        }
      },
      "InitEmbedRequest": {
        "type": "object",
        "required": [
          "parent_origin"
        ],
        "properties": {
          "parent_origin": {
            "type": "string",
            "description": "Origin of the brand page that opened the window, e.g. https://shop.example"
          }
        }
      },
      "IssueParams": {
        "type": "object",
        "description": "The values in `message`, for clients that word the finding in their own language (keys depend on `code`, e.g. value_mm, min_mm, max_mm, prev_size, fiber, max). Numbers are plain decimals (\".\" separator); lengths in mm.",
        "additionalProperties": {
          "type": "string"
        }
      },
      "LinkProductsRequest": {
        "type": "object",
        "required": [
          "product_ids"
        ],
        "properties": {
          "product_ids": {
            "type": "array",
            "minItems": 1,
            "maxItems": 500,
            "items": {
              "type": "string",
              "format": "uuid"
            }
          }
        }
      },
      "Measurement": {
        "type": "object",
        "required": [
          "code",
          "value_mm",
          "source"
        ],
        "properties": {
          "code": {
            "$ref": "#/components/schemas/MeasurementCode"
          },
          "value_mm": {
            "type": "number",
            "format": "double",
            "description": "Measured value in millimetres."
          },
          "source": {
            "type": "string",
            "enum": [
              "tape_self",
              "tape_helper",
              "flat_garment",
              "estimated"
            ]
          }
        }
      },
      "MeasurementCode": {
        "type": "string",
        "description": "Body measurement from set v1.",
        "enum": [
          "neck_girth",
          "shoulder_width",
          "arm_length",
          "armscye_girth",
          "upper_arm_girth",
          "wrist_girth",
          "chest_girth",
          "underbust_girth",
          "bust_point_to_bust_point",
          "shoulder_to_bust_point",
          "back_length",
          "waist_girth",
          "front_waist_arc",
          "high_hip_girth",
          "abdomen_girth",
          "hip_girth",
          "front_hip_arc",
          "thigh_girth",
          "knee_girth",
          "calf_girth",
          "ankle_girth",
          "inseam",
          "outseam",
          "crotch_length"
        ]
      },
      "OutcomeLineInput": {
        "type": "object",
        "required": [
          "id",
          "product",
          "size",
          "quantity"
        ],
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "string",
            "maxLength": 200,
            "description": "Your order line ID (unique within the order)."
          },
          "product": {
            "type": "string",
            "maxLength": 200,
            "description": "Your product ID (the one you pass to the SDK)."
          },
          "variant": {
            "type": "string",
            "maxLength": 200
          },
          "size": {
            "type": "string",
            "maxLength": 40,
            "description": "The size bought, in your labels."
          },
          "quantity": {
            "type": "integer",
            "minimum": 1,
            "maximum": 1000
          },
          "unit_price_minor": {
            "type": "integer",
            "format": "int64",
            "minimum": 0,
            "description": "Price of one unit in minor units (5995 = 59.95). Optional; used for the refund value estimate."
          },
          "tys_shown": {
            "type": "boolean",
            "description": "A TryYourSize element was shown for this purchase."
          },
          "recommended_size": {
            "type": "string",
            "maxLength": 40,
            "description": "The size TryYourSize recommended (the SDK's recommendation event, or the _tys_size cart line property)."
          },
          "attribution": {
            "type": "string",
            "enum": [
              "cart_attribute",
              "sdk",
              "api"
            ],
            "description": "Where recommended_size came from."
          }
        }
      },
      "OutcomeOrderInput": {
        "description": "An order from the brand's server. Re-sending the same id updates it (idempotent). No customer data: there is no customer, email, name or address field, and unknown fields are refused.",
        "type": "object",
        "required": [
          "id",
          "ordered_at",
          "lines"
        ],
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "string",
            "maxLength": 200,
            "description": "Your order ID."
          },
          "ordered_at": {
            "type": "string",
            "format": "date-time"
          },
          "currency": {
            "type": "string",
            "description": "ISO 4217, e.g. EUR."
          },
          "visitor_id": {
            "type": "string",
            "maxLength": 64,
            "description": "The SDK's anonymous visitor ID (`(await tys.experiment())?.visitorId`), saved with the cart. TryYourSize recomputes the A/B arm from it and stores only its hash. Never a customer ID."
          },
          "experiment_arm": {
            "type": "string",
            "enum": [
              "treatment",
              "holdout"
            ],
            "description": "The arm, when you cannot send visitor_id."
          },
          "lines": {
            "type": "array",
            "minItems": 1,
            "maxItems": 200,
            "items": {
              "$ref": "#/components/schemas/OutcomeLineInput"
            }
          }
        }
      },
      "OutcomeOrderResult": {
        "type": "object",
        "required": [
          "id",
          "external_id",
          "created",
          "lines"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "external_id": {
            "type": "string"
          },
          "created": {
            "type": "boolean",
            "description": "False when the order already existed and was updated."
          },
          "experiment_arm": {
            "type": "string",
            "enum": [
              "treatment",
              "holdout"
            ]
          },
          "lines": {
            "type": "integer"
          }
        }
      },
      "OutcomeReturnInput": {
        "description": "A return of (part of) an order line. Idempotent by (line, id). Free-text notes are not accepted.",
        "type": "object",
        "required": [
          "id",
          "order_id",
          "line_id",
          "quantity",
          "returned_at"
        ],
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "string",
            "maxLength": 200,
            "description": "Your return (or RMA) ID."
          },
          "order_id": {
            "type": "string",
            "maxLength": 200
          },
          "line_id": {
            "type": "string",
            "maxLength": 200
          },
          "quantity": {
            "type": "integer",
            "minimum": 1,
            "maximum": 1000
          },
          "reason": {
            "type": "string",
            "maxLength": 60,
            "description": "size_too_small, size_too_large, fit_other, style, quality, damaged_or_wrong, changed_mind, other; Shopify's ReturnReason codes (SIZE_TOO_SMALL…) and common returns-platform words are mapped. Empty: unspecified."
          },
          "exchange": {
            "type": "boolean",
            "description": "The item was exchanged rather than refunded."
          },
          "exchange_size": {
            "type": "string",
            "maxLength": 40,
            "description": "The size sent in exchange (implies exchange)."
          },
          "returned_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "OutcomeReturnResult": {
        "type": "object",
        "required": [
          "created",
          "reason"
        ],
        "properties": {
          "created": {
            "type": "boolean"
          },
          "reason": {
            "type": "string",
            "description": "The reason as normalised to TryYourSize's taxonomy."
          }
        }
      },
      "POMTable": {
        "type": "object",
        "description": "Garment measurements per size: size label → POM code → millimetres. Widths are flat half-measurements. See GET /v1/studio/garment_dictionary for codes and plausible ranges.",
        "additionalProperties": {
          "type": "object",
          "additionalProperties": {
            "type": "number",
            "format": "double"
          }
        }
      },
      "Product": {
        "type": "object",
        "required": [
          "id",
          "external_id",
          "name",
          "category",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "external_id": {
            "type": "string",
            "description": "The brand's SKU or style ID (unique per brand)."
          },
          "name": {
            "type": "string"
          },
          "category": {
            "$ref": "#/components/schemas/GarmentCategory"
          },
          "published_version": {
            "type": "integer",
            "description": "Garment version shoppers are fitted against, if any."
          },
          "draft_version": {
            "type": "integer",
            "description": "Garment version being edited, if any."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "fit_group_id": {
            "type": "string",
            "format": "uuid",
            "description": "The fit group whose garment this product uses (absent until grouped)."
          },
          "brand_id": {
            "type": "string",
            "format": "uuid",
            "description": "The brand the product belongs to; every store of the brand sees it."
          },
          "home_store_id": {
            "type": "string",
            "format": "uuid",
            "description": "The store the product was created in."
          },
          "retired_at": {
            "type": "string",
            "format": "date-time",
            "description": "Set when the product is retired; shoppers no longer get a size for it."
          },
          "available": {
            "type": "boolean",
            "description": "Whether the store it was read through offers it (false in the brand list, see `stores`)."
          }
        }
      },
      "ProductInput": {
        "type": "object",
        "required": [
          "external_id",
          "name",
          "category"
        ],
        "properties": {
          "external_id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "category": {
            "$ref": "#/components/schemas/GarmentCategory"
          }
        }
      },
      "ProductPage": {
        "type": "object",
        "description": "One page of a list (convention). Pass `next_cursor` as `cursor` for the next page while `has_more` is true.",
        "required": [
          "data",
          "has_more"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Product"
            }
          },
          "has_more": {
            "type": "boolean"
          },
          "next_cursor": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "ProductSizeMap": {
        "type": "object",
        "description": "Studio size label → the shop's variant option value (\"M\" → \"Medium\"). Must cover every variant to publish.",
        "required": [
          "entries"
        ],
        "properties": {
          "entries": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "studio_size",
                "shop_value"
              ],
              "properties": {
                "studio_size": {
                  "type": "string"
                },
                "shop_value": {
                  "type": "string"
                }
              }
            }
          },
          "unmapped_shop_values": {
            "type": "array",
            "description": "Variant size values of the product with no Studio size yet (read only).",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "ProductTryOn": {
        "type": "object",
        "description": "Whether the store offers \"See it on you\" for a product. `available` is false when the store has no published garment for that product (SKU), or its category cannot be simulated yet; the other fields are then omitted.",
        "required": [
          "available"
        ],
        "properties": {
          "available": {
            "type": "boolean"
          },
          "product": {
            "type": "string",
            "description": "The product's SKU / style ID, as given."
          },
          "garment_name": {
            "type": "string"
          },
          "category": {
            "$ref": "#/components/schemas/GarmentCategory"
          },
          "sizes": {
            "type": "array",
            "description": "The garment's size labels as entered in Studio, smallest to largest. Try-on results and `summary.recommended_size` use these.",
            "items": {
              "type": "string"
            }
          },
          "shop_sizes": {
            "type": "array",
            "description": "The store's own label for each entry of `sizes`, same order (the product's size map, e.g. \"M\" → \"Medium\"; the Studio label where the map has no entry).",
            "items": {
              "type": "string"
            }
          },
          "garment_version_id": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "PublicProductAppearance": {
        "type": "object",
        "description": "What the shopper's window shows on the simulated garment: the product's colourways and their artwork. Product data only (no shopper data). Switching colourway is instant (no new try-on). `colourways` is empty when the product has no appearance (or is unknown).",
        "required": [
          "product",
          "colourways",
          "assets"
        ],
        "properties": {
          "product": {
            "type": "string"
          },
          "colourways": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AppearanceColourway"
            }
          },
          "selected": {
            "type": "integer",
            "description": "Index into `colourways` of the one matching `?colour=` (else 0, the default). Absent when there are none."
          },
          "assets": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AppearanceAsset"
            }
          }
        }
      },
      "RecommendationHandle": {
        "type": "object",
        "description": "An opaque, sealed handle for one signed-in shopper on one store and mode. Pass it to your server, which sends it to POST /v1/recommendations with a secret key. It is encrypted: neither your page nor your server can read anything from it, and it never carries the shopper's email. It expires after an hour.",
        "required": [
          "handle",
          "expires_at",
          "mode"
        ],
        "properties": {
          "handle": {
            "type": "string",
            "description": "Starts with `tys_rh_`. Up to 4 KB.",
            "example": "tys_rh_AbC…"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "mode": {
            "type": "string",
            "enum": [
              "test",
              "live"
            ]
          }
        }
      },
      "RecommendationHandleRequest": {
        "type": "object",
        "description": "The signed-in shopper's body as the TryYourSize window holds it (the same fields as POST /v1/tryon, without the product). Sent only by our window (status frame) or SDKs, never by a brand's page or server.",
        "required": [
          "basics",
          "body_state"
        ],
        "properties": {
          "basics": {
            "$ref": "#/components/schemas/BodyBasics"
          },
          "measurements": {
            "type": "array",
            "maxItems": 64,
            "items": {
              "$ref": "#/components/schemas/Measurement"
            }
          },
          "body_state": {
            "type": "array",
            "minItems": 1,
            "maxItems": 64,
            "items": {
              "type": "number",
              "format": "double"
            }
          }
        }
      },
      "RecommendationList": {
        "type": "object",
        "required": [
          "data",
          "mode"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ServerRecommendation"
            }
          },
          "mode": {
            "type": "string",
            "enum": [
              "test",
              "live"
            ]
          }
        }
      },
      "RecommendationsRequest": {
        "type": "object",
        "required": [
          "handle",
          "products"
        ],
        "properties": {
          "handle": {
            "type": "string",
            "minLength": 8,
            "maxLength": 8192,
            "description": "A recommendation handle from the browser SDK (`tys.recommendationHandle()`)."
          },
          "products": {
            "type": "array",
            "minItems": 1,
            "maxItems": 25,
            "description": "Product SKUs / style IDs (what you pass to the element as `product`).",
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 120
            }
          }
        }
      },
      "ServerRecommendation": {
        "type": "object",
        "description": "The shopper's size for one product: sizes only, never measurements. Same statuses as the browser SDK's `recommendation` event, minus `signed_out` (a handle always belongs to a signed-in shopper).",
        "required": [
          "product",
          "status",
          "size",
          "studio_size",
          "wearable_sizes"
        ],
        "properties": {
          "product": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "ready",
              "computing",
              "none",
              "unavailable"
            ],
            "description": "`ready`: a size. `computing`: a simulation the shopper started is running (ask again later). `none`: simulated, no size can be worn. `unavailable`: no published garment for this product, not simulated on this body yet, or the store's plan has no fit checks left. A page view or this API never starts a simulation."
          },
          "size": {
            "type": "string",
            "nullable": true,
            "description": "The recommended size in the store's label (the product's size map), or null."
          },
          "studio_size": {
            "type": "string",
            "nullable": true,
            "description": "The same size as entered in TryYourSize Studio, or null."
          },
          "wearable_sizes": {
            "type": "array",
            "description": "Sizes that can be worn on this body, in the store's labels.",
            "items": {
              "type": "string"
            }
          },
          "method": {
            "type": "string",
            "enum": [
              "estimate",
              "simulated"
            ],
            "description": "How the size was determined: `estimate` (a size estimate certain for this body, no try-on of it yet) or `simulated` (from a try-on of this body). A coarse informational label: do not build logic on its values."
          },
          "tryon_id": {
            "type": "string",
            "format": "uuid",
            "description": "The try-on that produced the size, when there is one. Send it back with orders for attribution."
          }
        }
      },
      "SpecSource": {
        "type": "object",
        "description": "Where the POM table came from, kept for audit. Set by Studio's paste/import.",
        "required": [
          "kind"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "manual",
              "paste",
              "xlsx",
              "csv",
              "pdf",
              "workbook",
              "clo",
              "browzwear"
            ]
          },
          "filename": {
            "type": "string",
            "maxLength": 255
          }
        }
      },
      "TolerancesMM": {
        "type": "object",
        "description": "Spec-sheet tolerance per POM code, ± mm (e.g. {\"chest_width\": 10}). Optional; the flat preview uses ±5 mm when a POM has none.",
        "additionalProperties": {
          "type": "number",
          "format": "double"
        }
      },
      "TryOnJob": {
        "type": "object",
        "description": "A try-on: every size of the product's published garment shown on the body. Poll `GET /v1/tryon/{tryon_id}` every ~2 s until `status` is `done` or `failed`. A repeated try-on of the same garment version and body answers at once.",
        "required": [
          "id",
          "status",
          "progress"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "The try-on's id (uuid). Empty only for a size estimate (POST ?start=false&view=summary answered with no try-on of this body); there is nothing to poll."
          },
          "viewer_token": {
            "type": "string",
            "description": "Only in the answer to POST /v1/tryon: the window's proof that it may read this try-on. Send it as `Authorization: Bearer tys_view_…` to GET /v1/tryon/{tryon_id}. Stored hashed; never sent again."
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "running",
              "done",
              "failed"
            ]
          },
          "progress": {
            "type": "object",
            "required": [
              "sizes_done",
              "sizes_total"
            ],
            "properties": {
              "sizes_done": {
                "type": "integer"
              },
              "sizes_total": {
                "type": "integer"
              }
            }
          },
          "result": {
            "type": "object",
            "additionalProperties": true,
            "description": "Present when `status` is `done`, and while it runs: the sizes computed so far, each with `method` (simulated) and `state` (preview: shown early, may still change; settled; refining: still being computed, no mesh yet; simulated). The try-on result for the window: body mesh and body_state (no basics, no tape measurements), garment with its fabric and estimated POMs, recommended size and per-size meshes, pressure kPa, ease mm and zones. Binary arrays are base64 little-endian; per-vertex values are rounded to display precision."
          },
          "summary": {
            "type": "object",
            "description": "Present when `status` is `done` and the request asked for `view=summary`: what a store may receive (sizes only, never measurements). Size labels are the garment's (Studio) sizes; map them with `shop_sizes` from GET /v1/products/{product}/tryon.",
            "required": [
              "no_size_fits",
              "wearable_sizes"
            ],
            "properties": {
              "recommended_size": {
                "type": "string",
                "description": "The recommended size; absent when no size can be worn."
              },
              "no_size_fits": {
                "type": "boolean"
              },
              "wearable_sizes": {
                "type": "array",
                "description": "Sizes that can be worn on this body (judged on this body), smallest to largest.",
                "items": {
                  "type": "string"
                }
              },
              "method": {
                "type": "string",
                "enum": [
                  "estimate",
                  "simulated"
                ],
                "description": "How the size was determined: `estimate` (a size estimate, no try-on of this body yet) or `simulated` (from the try-on of this body). A coarse informational label: do not build logic on its values."
              }
            }
          },
          "recommendation": {
            "type": "object",
            "description": "The size answer with its method. On the first answer of a try-on it may already be there (`method: estimate`) while the 3D view is computed; once done it is the try-on's own size. An estimate is final (`ready` or `none`) only when it is certain for this body; otherwise the status is `computing`, no size is named, and the try-on's 3D view gives it.",
            "required": [
              "method",
              "status"
            ],
            "properties": {
              "recommended_size": {
                "type": "string",
                "nullable": true
              },
              "method": {
                "type": "string",
                "enum": [
                  "estimate",
                  "simulated"
                ],
                "description": "Same as `summary.method`. A coarse informational label; do not build logic on its values."
              },
              "status": {
                "type": "string",
                "enum": [
                  "ready",
                  "none",
                  "computing",
                  "unavailable"
                ]
              },
              "wearable_sizes": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          },
          "error": {
            "type": "object",
            "description": "Present when `status` is `failed`.",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "INVALID_BODY, UNSUPPORTED_CATEGORY, MISSING_POM, INVALID_POM, INVALID_FABRIC, NO_SIZES, UNAVAILABLE or ENGINE_ERROR."
              },
              "message": {
                "type": "string"
              }
            }
          }
        }
      },
      "TryOnRequest": {
        "type": "object",
        "description": "The product and the shopper's body as on screen: the same body as `/v1/bodies/preview`, with the preview's `body_state` (required; opaque).",
        "required": [
          "product",
          "basics",
          "body_state"
        ],
        "properties": {
          "product": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120,
            "description": "The product's SKU / style ID in the store (what the shop passes to the element)."
          },
          "basics": {
            "$ref": "#/components/schemas/BodyBasics"
          },
          "measurements": {
            "type": "array",
            "maxItems": 64,
            "items": {
              "$ref": "#/components/schemas/Measurement"
            }
          },
          "body_state": {
            "type": "array",
            "maxItems": 64,
            "description": "The `body_state` of the body preview on screen.",
            "items": {
              "type": "number",
              "format": "double"
            }
          }
        }
      }
    },
    "securitySchemes": {
      "PublishableKey": {
        "type": "apiKey",
        "in": "header",
        "name": "TYS-Publishable-Key",
        "description": "The store's publishable key (pk_test_… / pk_live_…). Safe in browsers; works only from the origins registered for the key."
      },
      "SecretKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "A secret key (sk_test_… / sk_live_…) or a restricted key (rk_test_… / rk_live_…) from your server: `Authorization: Bearer sk_…`. A secret key has every scope; a restricted key only the scopes it was created with. Each operation lists its key scopes."
      }
    }
  }
}
