{
  "openapi": "3.1.0",
  "info": {"title": "PRFCT.ART Merch Maker", "version": "1.0.0", "description": "Indicative RUB quotes and private requests awaiting human confirmation. No payment or automatic production. Amounts are integer kopecks. Null shipping/total means a separate delivery quote is required."},
  "servers": [{"url": "https://merch.prfct.art"}],
  "paths": {
    "/api/health": {"get": {"operationId": "health", "responses": {"200": {"description": "Service and storage configuration status"}}}},
    "/api/catalog": {"get": {"operationId": "catalog", "responses": {"200": {"description": "Own indicative tariffs, products, sizes, print areas, quantity tiers, country and delivery options; planned products are not orderable"}}}},
    "/api/quote": {"post": {"operationId": "quote", "description": "No contact or artwork bytes are required. Prints must fit the product's print area after rotation.", "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "required": ["selection"], "properties": {"selection": {"$ref": "#/components/schemas/Selection"}}}}}}, "responses": {"200": {"description": "Indicative quote with quoteId, tariffVersion, quantity, unitBaseMinor, unitPrintMinor, discountPercent, unitPriceMinor, itemsTotalMinor, shipping.amountMinor, totalMinor, productionDays and warnings"}, "400": {"description": "Invalid input"}, "429": {"description": "Rate limited"}}}},
    "/api/orders": {"post": {"operationId": "createRequest", "description": "Requires customer confirmation. Stores a private pending_confirmation request. Retries with the same key and identical body return the same request. No public GET endpoint.", "parameters": [{"in": "header", "name": "Idempotency-Key", "required": true, "schema": {"type": "string", "format": "uuid"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "required": ["selection", "project", "acceptedQuoteId", "customer", "consent", "artworkRights"], "properties": {"selection": {"$ref": "#/components/schemas/Selection"}, "project": {"$ref": "#/components/schemas/Project"}, "acceptedQuoteId": {"type": "string"}, "customer": {"type": "object", "required": ["name", "contact"], "properties": {"name": {"type": "string", "minLength": 2, "maxLength": 100}, "contact": {"type": "string", "maxLength": 160, "description": "Email, phone or Telegram handle"}, "comment": {"type": "string", "maxLength": 1000}}}, "consent": {"const": true}, "artworkRights": {"const": true}}}}}}, "responses": {"201": {"description": "Request persisted; requestId, status, quote, createdAt"}, "200": {"description": "Identical existing request (idempotent retry)"}, "400": {"description": "Invalid project, dimensions, contact or missing consent"}, "403": {"description": "Unapproved browser Origin"}, "409": {"description": "Quote changed or idempotency key reused for a different request"}, "413": {"description": "Request body too large"}, "429": {"description": "Rate limited"}, "503": {"description": "Storage unavailable; request not accepted"}}}}
  },
  "components": {"schemas": {
    "Selection": {"type": "object", "required": ["productId", "sizes", "prints", "destination", "deliveryMethod"], "properties": {"productId": {"const": "tee-classic"}, "sizes": {"type": "object", "description": "XS, S, M, L, XL, 2XL, 3XL; total 1..1000", "additionalProperties": {"type": "integer", "minimum": 0, "maximum": 1000}}, "prints": {"type": "array", "minItems": 1, "maxItems": 2, "items": {"type": "object", "required": ["side", "widthCm", "heightCm", "xCm", "yCm", "rotation"], "properties": {"side": {"enum": ["front", "back"]}, "widthCm": {"type": "number", "exclusiveMinimum": 0}, "heightCm": {"type": "number", "exclusiveMinimum": 0}, "xCm": {"type": "number"}, "yCm": {"type": "number"}, "rotation": {"type": "number", "description": "Clockwise degrees"}}}}, "destination": {"type": "object", "required": ["countryCode", "city", "postalCode"], "properties": {"countryCode": {"enum": ["RU", "KZ", "BY", "AM", "GE", "TR", "US", "DE", "OTHER"]}, "city": {"type": "string"}, "postalCode": {"type": "string"}}}, "deliveryMethod": {"enum": ["pickup_point", "courier"]}}},
    "Asset": {"type": ["object", "null"], "description": "Embedded PNG/JPEG/WebP image or self-contained static GLB. URLs to external files are not accepted.", "properties": {"name": {"type": "string"}, "dataUrl": {"type": "string"}, "width": {"type": "integer"}, "height": {"type": "integer"}}, "required": ["name", "dataUrl"]},
    "Surface": {"type": "object", "required": ["photo", "logo", "placement", "zone"], "properties": {"photo": {"$ref": "#/components/schemas/Asset"}, "logo": {"$ref": "#/components/schemas/Asset"}, "placement": {"type": "object", "required": ["xCm", "yCm", "widthCm", "rotation"], "properties": {"xCm": {"type": "number"}, "yCm": {"type": "number"}, "widthCm": {"type": "number"}, "rotation": {"type": "number"}}}, "zone": {"type": "object", "description": "Normalized rectangle contained in photo, x/y center", "required": ["x", "y", "width", "height"], "properties": {"x": {"type": "number"}, "y": {"type": "number"}, "width": {"type": "number"}, "height": {"type": "number"}}}}},
    "Project": {"type": "object", "description": "Same portable v1 JSON saved by the editor. Server verifies image dimensions and derives billed print sizes from actual file aspect ratio.", "required": ["version", "name", "mode", "side", "color", "printWidthCm", "printHeightCm", "garmentHeightCm", "modelYaw", "modelPitch", "zoneOffsetCm", "model", "surfaces"], "properties": {"version": {"const": 1}, "name": {"type": "string"}, "mode": {"enum": ["photo", "3d"]}, "side": {"enum": ["front", "back"]}, "color": {"type": "string", "pattern": "^#[a-fA-F0-9]{6}$"}, "printWidthCm": {"const": 30}, "printHeightCm": {"const": 40}, "garmentHeightCm": {"type": "number"}, "modelYaw": {"type": "number"}, "modelPitch": {"type": "number"}, "zoneOffsetCm": {"type": "number"}, "model": {"$ref": "#/components/schemas/Asset"}, "surfaces": {"type": "object", "required": ["front", "back"], "properties": {"front": {"$ref": "#/components/schemas/Surface"}, "back": {"$ref": "#/components/schemas/Surface"}}}}}
  }}
}
