{
  "openapi": "3.1.0",
  "info": {
    "title": "Mercastra REST API",
    "version": "0.20.0"
  },
  "servers": [
    {
      "url": "https://mercastra.cloud/api"
    }
  ],
  "paths": {
    "/authenticate": {
      "post": {
        "operationId": "authenticate",
        "tags": [
          "Session"
        ],
        "summary": "Sign in with email and password",
        "description": "The only way a script obtains a session today. On success sets two cookies: `session` (httpOnly, carries the session id) and `csrf-token` (readable by JavaScript). Every mutating request afterwards - anything but GET, HEAD and OPTIONS - must send the `csrf-token` cookie's value back as the `X-CSRF-Token` header, or it is refused with 403; GET requests need only the `session` cookie. A cookie jar makes this workable from a script; a browser on another origin cannot call it (no CORS, `SameSite=Lax` cookies). Responds 400 when the body fails validation and 401 when the email or password is wrong.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
                  },
                  "password": {
                    "type": "string"
                  }
                },
                "required": [
                  "email",
                  "password"
                ]
              },
              "example": {
                "email": "kamil@example.com",
                "password": "correct horse battery staple"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthenticateResponse"
                },
                "example": {
                  "user": {
                    "uid": "uid-1",
                    "email": "kamil@example.com",
                    "name": "Kamil",
                    "locale": "en"
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request body does not match the schema"
          },
          "401": {
            "description": "Not authenticated"
          }
        },
        "x-stability": "experimental",
        "x-rate-limit": {
          "max": 10,
          "windowSeconds": 900
        }
      }
    },
    "/logout": {
      "post": {
        "operationId": "logout",
        "tags": [
          "Session"
        ],
        "summary": "End the caller's session",
        "description": "Revokes the session behind the `session` cookie and clears both auth cookies. Responds 403 when the CSRF token is missing or invalid.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Not authenticated"
          },
          "403": {
            "description": "Forbidden"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      }
    },
    "/me": {
      "get": {
        "operationId": "getMe",
        "tags": [
          "Account"
        ],
        "summary": "Read the signed-in account's profile",
        "description": "Returns the caller's own name, email, locale and the sign-in methods linked to the account.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MeProfile"
                },
                "example": {
                  "name": "Kamil",
                  "email": "kamil@example.com",
                  "locale": "en",
                  "signInMethods": [
                    "password"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated"
          },
          "404": {
            "description": "Not found"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      },
      "patch": {
        "operationId": "patchMe",
        "tags": [
          "Account"
        ],
        "summary": "Update the signed-in account's profile",
        "description": "Updates the caller's own name and/or locale; at least one of the two fields is required. The body is strict, an unknown field is a 400.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 100,
                    "pattern": "^[\\p{L}\\p{M}\\p{Nd} '’.,\\-&\\u200d\\ufe0f\\p{Emoji_Modifier}\\p{Extended_Pictographic}]+$",
                    "description": "At most 100 characters; letters, marks, digits, spaces and a small set of punctuation"
                  },
                  "locale": {
                    "$ref": "#/components/schemas/UserLocale"
                  }
                },
                "additionalProperties": false
              },
              "example": {
                "locale": "en"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MeProfile"
                },
                "example": {
                  "name": "Kamil",
                  "email": "kamil@example.com",
                  "locale": "en",
                  "signInMethods": [
                    "password"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "The request body does not match the schema",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated"
          },
          "404": {
            "description": "Not found"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      }
    },
    "/me/data-export": {
      "get": {
        "operationId": "getMeDataExport",
        "tags": [
          "Account"
        ],
        "summary": "Export everything stored about the account",
        "description": "Returns the account record, every workshop it owns or belongs to with that workshop's grids, estimates (with revisions) and orders, every feedback or support message it submitted, and its linked OAuth providers. Documents what the export contains today, not what it should; MR-44 tracks what it omits.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountDataExport"
                },
                "example": {
                  "user": {
                    "uid": "uid-1",
                    "email": "kamil@example.com",
                    "name": "Kamil",
                    "status": "active",
                    "emailVerifiedAt": "2026-07-01T00:00:00.000Z",
                    "termsVersion": "2026-01-01",
                    "termsAcceptedAt": "2026-07-01T00:00:00.000Z",
                    "lastLoginAt": "2026-09-15T08:00:00.000Z",
                    "locale": "en"
                  },
                  "workshops": [],
                  "feedback": [],
                  "authProviders": []
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated"
          },
          "404": {
            "description": "Not found"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      }
    },
    "/workshops/{workshopId}/context": {
      "put": {
        "operationId": "putWorkshopContext",
        "tags": [
          "Workshops"
        ],
        "summary": "Replace the workshop's context notes",
        "description": "Stores free-text notes describing the workshop: what it makes, how it works and anything Mercastra should know to tailor its work to it, price grids or customer messages for example. Whitespace around the notes is trimmed and the whole value is replaced, an empty string clears it. The notes feed summaries derived later, saving them triggers nothing by itself. Responds 403 when the caller is not a member of the workshop and 404 when no such workshop exists.",
        "parameters": [
          {
            "name": "workshopId",
            "in": "path",
            "required": true,
            "description": "Workshop id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "notes": {
                    "type": "string",
                    "maxLength": 4000,
                    "description": "Free-text notes about the workshop, trimmed, at most 4000 UTF-16 code units"
                  }
                },
                "required": [
                  "notes"
                ],
                "additionalProperties": false
              },
              "example": {
                "notes": "I build solid-wood furniture to order. Two-week lead time."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WorkshopContext"
                },
                "example": {
                  "notes": "I build solid-wood furniture to order. Two-week lead time.",
                  "updatedAt": "2026-09-05T10:00:00.000Z"
                }
              }
            }
          },
          "400": {
            "description": "The request body does not match the schema",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not found"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      }
    },
    "/workshops": {
      "get": {
        "operationId": "getWorkshops",
        "tags": [
          "Workshops"
        ],
        "summary": "List the caller's workshops",
        "description": "Returns every workshop the caller is a member of. Each workshop carries its resolved `plan`, its `context` when notes have been saved and, once synthesized, its `contextDerived`.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WorkshopsList"
                },
                "example": {
                  "workshops": [
                    {
                      "id": "workshop-1",
                      "name": "Extremely Awesome Workshop",
                      "tagline": "Solid-wood furniture, built to order",
                      "logo": "🛠️",
                      "crafterName": "Kamil",
                      "crafterAvatarSeed": "uid-1",
                      "background": "paper",
                      "titleFont": "sans",
                      "accentColor": null,
                      "plan": "free",
                      "planExpiresAt": null,
                      "planSource": null,
                      "isDefault": true,
                      "members": [
                        {
                          "id": "uid-1",
                          "name": "Kamil",
                          "avatarSeed": "uid-1",
                          "role": "owner"
                        }
                      ],
                      "createdAt": "2026-07-04T00:00:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      }
    },
    "/workshops/{workshopId}": {
      "patch": {
        "operationId": "patchWorkshop",
        "tags": [
          "Workshops"
        ],
        "summary": "Update the workshop's profile",
        "description": "Updates the workshop's profile fields: name, tagline, logo, crafter name, background, custom background, title font and accent color. The body is strict, an unknown field is a 400 - `plan` is never accepted here, it is written only by the founder-side plan grant tooling. `background` and `titleFont` are preset ids validated against a fixed list; `customBackground` and `accentColor` are freeform #RRGGBB hex, with null/absent meaning no override. Responds 403 when the caller is not a member of the workshop and 404 when no such workshop exists.",
        "parameters": [
          {
            "name": "workshopId",
            "in": "path",
            "required": true,
            "description": "Workshop id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1
                  },
                  "tagline": {
                    "description": "At most 250 characters",
                    "type": "string",
                    "maxLength": 250
                  },
                  "logo": {
                    "$ref": "#/components/schemas/Emoji"
                  },
                  "crafterName": {
                    "type": "string"
                  },
                  "background": {
                    "$ref": "#/components/schemas/Background"
                  },
                  "customBackground": {
                    "$ref": "#/components/schemas/WorkshopCustomBackground"
                  },
                  "titleFont": {
                    "$ref": "#/components/schemas/TitleFont"
                  },
                  "accentColor": {
                    "anyOf": [
                      {
                        "$ref": "#/components/schemas/HexColor"
                      },
                      {
                        "type": "null"
                      }
                    ]
                  }
                },
                "additionalProperties": false
              },
              "example": {
                "tagline": "Solid-wood furniture, built to order",
                "accentColor": "#6D5BD0"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Workshop"
                },
                "example": {
                  "id": "workshop-1",
                  "name": "Extremely Awesome Workshop",
                  "tagline": "Solid-wood furniture, built to order",
                  "logo": "🛠️",
                  "crafterName": "Kamil",
                  "crafterAvatarSeed": "uid-1",
                  "background": "paper",
                  "titleFont": "sans",
                  "accentColor": "#6D5BD0",
                  "plan": "free",
                  "planExpiresAt": null,
                  "planSource": null,
                  "isDefault": true,
                  "members": [
                    {
                      "id": "uid-1",
                      "name": "Kamil",
                      "avatarSeed": "uid-1",
                      "role": "owner"
                    }
                  ],
                  "createdAt": "2026-07-04T00:00:00.000Z"
                }
              }
            }
          },
          "400": {
            "description": "The request body does not match the schema",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not found"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      }
    },
    "/workshops/{workshopId}/entitlements": {
      "get": {
        "operationId": "getWorkshopEntitlements",
        "tags": [
          "Workshops"
        ],
        "summary": "Read the workshop's plan entitlements and quota usage",
        "description": "Read-only view of the workshop's plan entitlements and quota usage - what the plan-management page renders. Never calls the payment provider, everything is derived from stored state. Responds 403 when the caller is not a member of the workshop and 404 when no such workshop exists.",
        "parameters": [
          {
            "name": "workshopId",
            "in": "path",
            "required": true,
            "description": "Workshop id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WorkshopEntitlementsUsage"
                },
                "example": {
                  "plan": "free",
                  "planExpiresAt": null,
                  "brandMark": {
                    "canDisable": false
                  },
                  "fileStorage": {
                    "usedBytes": 5242880,
                    "limitBytes": 10485760
                  },
                  "revisions": {
                    "limitPerEstimate": 5
                  },
                  "billing": {
                    "hasAccount": false,
                    "planSource": null,
                    "cancelAtPeriodEnd": false,
                    "checkoutAvailability": "available"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not found"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      }
    },
    "/workshops/{workshopId}/orders": {
      "get": {
        "operationId": "getWorkshopOrders",
        "tags": [
          "Orders"
        ],
        "summary": "List a workshop's orders",
        "description": "Returns every order belonging to the workshop. Responds 403 when the caller is not a member of the workshop; there is no 404 for an unknown workshop id, a non-member sees 403 either way.",
        "parameters": [
          {
            "name": "workshopId",
            "in": "path",
            "required": true,
            "description": "Workshop id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrdersList"
                },
                "example": {
                  "orders": [
                    {
                      "id": "order-1",
                      "workshopId": "workshop-1",
                      "title": "Oak dining table",
                      "customerName": "Anna Kowalska",
                      "notes": "Six chairs to match, deliver by the 20th.",
                      "status": "in_progress",
                      "createdAt": "2026-08-01T09:00:00.000Z",
                      "lastActivityAt": "2026-08-10T14:30:00.000Z",
                      "statusHistory": [
                        {
                          "status": "todo",
                          "at": "2026-08-01T09:00:00.000Z"
                        },
                        {
                          "status": "in_progress",
                          "at": "2026-08-10T14:30:00.000Z"
                        }
                      ]
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated"
          },
          "403": {
            "description": "Forbidden"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      },
      "post": {
        "operationId": "postWorkshopOrder",
        "tags": [
          "Orders"
        ],
        "summary": "Create an order in a workshop",
        "description": "Creates a new order with status todo and an empty notes field. `customerName` defaults to an empty string when omitted, an order can have no named customer. Responds 403 when the caller is not a member of the workshop; there is no 404 for an unknown workshop id, a non-member sees 403 either way. The body is not strict, an unknown field is silently dropped rather than rejected.",
        "parameters": [
          {
            "name": "workshopId",
            "in": "path",
            "required": true,
            "description": "Workshop id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 250,
                    "pattern": "^[^\\p{Cc}]*$"
                  },
                  "customerName": {
                    "type": "string",
                    "maxLength": 250,
                    "pattern": "^[^\\p{Cc}]*$"
                  }
                },
                "required": [
                  "title"
                ]
              },
              "example": {
                "title": "Oak dining table",
                "customerName": "Anna Kowalska"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Order"
                },
                "example": {
                  "id": "order-1",
                  "workshopId": "workshop-1",
                  "title": "Oak dining table",
                  "customerName": "Anna Kowalska",
                  "notes": "Six chairs to match, deliver by the 20th.",
                  "status": "in_progress",
                  "createdAt": "2026-08-01T09:00:00.000Z",
                  "lastActivityAt": "2026-08-10T14:30:00.000Z",
                  "statusHistory": [
                    {
                      "status": "todo",
                      "at": "2026-08-01T09:00:00.000Z"
                    },
                    {
                      "status": "in_progress",
                      "at": "2026-08-10T14:30:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "The request body does not match the schema",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated"
          },
          "403": {
            "description": "Forbidden"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      }
    },
    "/orders/{orderId}": {
      "get": {
        "operationId": "getOrder",
        "tags": [
          "Orders"
        ],
        "summary": "Read an order",
        "description": "Responds 403 when the caller is not a member of the order's workshop and 404 when no such order exists.",
        "parameters": [
          {
            "name": "orderId",
            "in": "path",
            "required": true,
            "description": "Order id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "order": {
                      "$ref": "#/components/schemas/Order"
                    }
                  },
                  "required": [
                    "order"
                  ],
                  "additionalProperties": false
                },
                "example": {
                  "order": {
                    "id": "order-1",
                    "workshopId": "workshop-1",
                    "title": "Oak dining table",
                    "customerName": "Anna Kowalska",
                    "notes": "Six chairs to match, deliver by the 20th.",
                    "status": "in_progress",
                    "createdAt": "2026-08-01T09:00:00.000Z",
                    "lastActivityAt": "2026-08-10T14:30:00.000Z",
                    "statusHistory": [
                      {
                        "status": "todo",
                        "at": "2026-08-01T09:00:00.000Z"
                      },
                      {
                        "status": "in_progress",
                        "at": "2026-08-10T14:30:00.000Z"
                      }
                    ]
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not found"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      },
      "patch": {
        "operationId": "patchOrder",
        "tags": [
          "Orders"
        ],
        "summary": "Update an order's title, customer name or notes",
        "description": "Updates any subset of title, customerName and notes; at least one is required, the body is strict and an unknown field is a 400. None of these three fields counts as order activity: lastActivityAt and statusHistory are untouched by this endpoint. Responds 403 when the caller is not a member of the order's workshop and 404 when no such order exists.",
        "parameters": [
          {
            "name": "orderId",
            "in": "path",
            "required": true,
            "description": "Order id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 250,
                    "pattern": "^[^\\p{Cc}]*$"
                  },
                  "customerName": {
                    "type": "string",
                    "maxLength": 250,
                    "pattern": "^[^\\p{Cc}]*$"
                  },
                  "notes": {
                    "type": "string",
                    "maxLength": 5000
                  }
                },
                "additionalProperties": false
              },
              "example": {
                "notes": "Six chairs to match, deliver by the 20th."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Order"
                },
                "example": {
                  "id": "order-1",
                  "workshopId": "workshop-1",
                  "title": "Oak dining table",
                  "customerName": "Anna Kowalska",
                  "notes": "Six chairs to match, deliver by the 20th.",
                  "status": "in_progress",
                  "createdAt": "2026-08-01T09:00:00.000Z",
                  "lastActivityAt": "2026-08-10T14:30:00.000Z",
                  "statusHistory": [
                    {
                      "status": "todo",
                      "at": "2026-08-01T09:00:00.000Z"
                    },
                    {
                      "status": "in_progress",
                      "at": "2026-08-10T14:30:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "The request body does not match the schema",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not found"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      }
    },
    "/orders/{orderId}/status": {
      "patch": {
        "operationId": "patchOrderStatus",
        "tags": [
          "Orders"
        ],
        "summary": "Change an order's status",
        "description": "Appends a statusHistory entry timestamped now and sets lastActivityAt to the same moment. Responds 403 when the caller is not a member of the order's workshop and 404 when no such order exists.",
        "parameters": [
          {
            "name": "orderId",
            "in": "path",
            "required": true,
            "description": "Order id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "status": {
                    "$ref": "#/components/schemas/OrderStatus"
                  }
                },
                "required": [
                  "status"
                ]
              },
              "example": {
                "status": "done"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Order"
                },
                "example": {
                  "id": "order-1",
                  "workshopId": "workshop-1",
                  "title": "Oak dining table",
                  "customerName": "Anna Kowalska",
                  "notes": "Six chairs to match, deliver by the 20th.",
                  "status": "done",
                  "createdAt": "2026-08-01T09:00:00.000Z",
                  "lastActivityAt": "2026-08-10T14:30:00.000Z",
                  "statusHistory": [
                    {
                      "status": "todo",
                      "at": "2026-08-01T09:00:00.000Z"
                    },
                    {
                      "status": "in_progress",
                      "at": "2026-08-10T14:30:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "The request body does not match the schema",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not found"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      }
    },
    "/files": {
      "post": {
        "operationId": "postFile",
        "tags": [
          "Files"
        ],
        "summary": "Upload a file",
        "description": "Uploads a file for a category and stores it against the workshop identified by scopeId. The response is the stored file's own record; this endpoint never assigns the file to anything else, a later request (for example a grid button patch) references it by id. Every upload also counts against the workshop's plan storage quota (10 MB on Free, 250 MB on paid plans) and the request itself is capped at 2 MB regardless of category, on top of the category's own per-file cap. 413 covers all three limits with an empty body; nothing in the response distinguishes which one was hit. The file's actual type is detected from its bytes, not the client-supplied MIME type.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "The file's bytes."
                  },
                  "category": {
                    "$ref": "#/components/schemas/FileCategory"
                  },
                  "scopeId": {
                    "type": "string",
                    "description": "The workshop id the file is uploaded against."
                  },
                  "name": {
                    "description": "An optional display name for the file.",
                    "type": "string",
                    "minLength": 1
                  }
                },
                "required": [
                  "file",
                  "category",
                  "scopeId"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FileRecord"
                },
                "example": {
                  "id": "file-1",
                  "scope": "workshop",
                  "scopeId": "workshop-1",
                  "category": "grid_tile_icon",
                  "name": "chisel-icon.png",
                  "contentType": "image/png",
                  "sizeBytes": 48213,
                  "uploadedByUid": "uid-1",
                  "createdAt": "2026-08-05T10:15:00.000Z"
                }
              }
            }
          },
          "400": {
            "description": "category or scopeId is missing or invalid, or no file was attached"
          },
          "401": {
            "description": "Not authenticated"
          },
          "403": {
            "description": "The caller is not a member of the workshop identified by scopeId"
          },
          "413": {
            "description": "The file exceeds the 2 MB request cap, the category's own size limit, or the workshop's storage quota"
          },
          "415": {
            "description": "The file's real content type is not one of the category's accepted types"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      },
      "get": {
        "operationId": "getFiles",
        "tags": [
          "Files"
        ],
        "summary": "List files in a category and scope",
        "description": "Both category and scopeId are required; there is no unscoped listing. Responds 403 when the caller is not a member of the workshop identified by scopeId.",
        "parameters": [
          {
            "name": "category",
            "in": "query",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/FileCategory"
            }
          },
          {
            "name": "scopeId",
            "in": "query",
            "required": true,
            "description": "The workshop id to list files for.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FilesList"
                },
                "example": {
                  "files": [
                    {
                      "id": "file-1",
                      "scope": "workshop",
                      "scopeId": "workshop-1",
                      "category": "grid_tile_icon",
                      "name": "chisel-icon.png",
                      "contentType": "image/png",
                      "sizeBytes": 48213,
                      "uploadedByUid": "uid-1",
                      "createdAt": "2026-08-05T10:15:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "category or scopeId is missing or invalid"
          },
          "401": {
            "description": "Not authenticated"
          },
          "403": {
            "description": "Forbidden"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      }
    },
    "/files/{fileId}": {
      "get": {
        "operationId": "getFile",
        "tags": [
          "Files"
        ],
        "summary": "Download a file's bytes",
        "description": "Streams the stored bytes with Cache-Control: private, max-age=31536000, immutable - a file's bytes never change once uploaded, only its record can be deleted. Responds 403 when the caller does not have access to the scope owning the file and 404 when no such file exists.",
        "parameters": [
          {
            "name": "fileId",
            "in": "path",
            "required": true,
            "description": "File id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success. The real Content-Type header reflects the file's actual type (one of image/jpeg, image/png, image/webp or application/pdf), not application/octet-stream.",
            "content": {
              "application/octet-stream": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not found"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      }
    },
    "/workshops/{workshopId}/estimates": {
      "get": {
        "operationId": "getWorkshopEstimates",
        "tags": [
          "Estimates"
        ],
        "summary": "List a workshop's estimates",
        "description": "Returns every estimate belonging to the workshop; the collection has no defined order. Responds 403 when the caller is not a member of the workshop; there is no 404 for an unknown workshop id, a non-member sees 403 either way.",
        "parameters": [
          {
            "name": "workshopId",
            "in": "path",
            "required": true,
            "description": "Workshop id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EstimatesList"
                },
                "example": {
                  "estimates": [
                    {
                      "id": "estimate-1",
                      "workshopId": "workshop-1",
                      "name": "Bespoke chair estimate",
                      "recipientName": "Anna Kowalska",
                      "createdAt": "2026-08-01T09:00:00.000Z",
                      "updatedAt": "2026-08-10T14:30:00.000Z",
                      "markupPercent": 10,
                      "discountPercent": 0,
                      "lineItems": [
                        {
                          "id": "line-item-1",
                          "label": "Oak plank, 2m",
                          "unit": "sqm",
                          "unitPrice": 120,
                          "quantity": 4
                        }
                      ]
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated"
          },
          "403": {
            "description": "Forbidden"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      },
      "post": {
        "operationId": "postWorkshopEstimate",
        "tags": [
          "Estimates"
        ],
        "summary": "Create an estimate in a workshop",
        "description": "Creates a new estimate. `name` defaults to \"Untitled estimate\" when omitted or blank after trimming; markupPercent and discountPercent default to 0 and lineItems to none, each submitted line item is assigned a fresh id. The body is not strict, an unknown field is silently dropped rather than rejected. Creating an estimate also writes its first revision (revisionNumber 1) immediately, before any call to confirm. Responds 403 when the caller is not a member of the workshop; there is no 404 for an unknown workshop id, a non-member sees 403 either way.",
        "parameters": [
          {
            "name": "workshopId",
            "in": "path",
            "required": true,
            "description": "Workshop id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "recipientName": {
                    "type": "string"
                  },
                  "markupPercent": {
                    "type": "number",
                    "minimum": 0
                  },
                  "discountPercent": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100
                  },
                  "lineItems": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "label": {
                          "type": "string",
                          "minLength": 1
                        },
                        "unit": {
                          "$ref": "#/components/schemas/LineItemUnit"
                        },
                        "unitPrice": {
                          "type": "number",
                          "minimum": 0
                        },
                        "quantity": {
                          "type": "number",
                          "exclusiveMinimum": 0
                        },
                        "sourceButtonId": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "label",
                        "unit",
                        "unitPrice",
                        "quantity"
                      ]
                    }
                  }
                }
              },
              "example": {
                "name": "Bespoke chair estimate",
                "lineItems": [
                  {
                    "label": "Oak plank, 2m",
                    "unit": "sqm",
                    "unitPrice": 120,
                    "quantity": 4
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Estimate"
                },
                "example": {
                  "id": "estimate-1",
                  "workshopId": "workshop-1",
                  "name": "Bespoke chair estimate",
                  "recipientName": "Anna Kowalska",
                  "createdAt": "2026-08-01T09:00:00.000Z",
                  "updatedAt": "2026-08-10T14:30:00.000Z",
                  "markupPercent": 10,
                  "discountPercent": 0,
                  "lineItems": [
                    {
                      "id": "line-item-1",
                      "label": "Oak plank, 2m",
                      "unit": "sqm",
                      "unitPrice": 120,
                      "quantity": 4
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "The request body does not match the schema",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated"
          },
          "403": {
            "description": "Forbidden"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      }
    },
    "/workshops/{workshopId}/estimates/most-used-line-items": {
      "get": {
        "operationId": "getWorkshopMostUsedLineItems",
        "tags": [
          "Estimates"
        ],
        "summary": "List the workshop's most-used line items",
        "description": "Aggregates every line item across the workshop's estimates by label and unit (case-insensitive, trimmed), ranked by how many estimates use them and, as a tiebreaker, by the most recent estimate update. An out-of-range or non-numeric limit responds 400 with an empty body, not a JSON error. Responds 403 when the caller is not a member of the workshop; there is no 404 for an unknown workshop id, a non-member sees 403 either way.",
        "parameters": [
          {
            "name": "workshopId",
            "in": "path",
            "required": true,
            "description": "Workshop id",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Defaults to 15",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MostUsedLineItemsList"
                },
                "example": {
                  "lineItems": [
                    {
                      "label": "Oak plank, 2m",
                      "unit": "sqm",
                      "unitPrice": 120,
                      "count": 6,
                      "lastUsedAt": "2026-08-10T14:30:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "limit is present but not an integer between 1 and 50"
          },
          "401": {
            "description": "Not authenticated"
          },
          "403": {
            "description": "Forbidden"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      }
    },
    "/estimates/{estimateId}": {
      "get": {
        "operationId": "getEstimate",
        "tags": [
          "Estimates"
        ],
        "summary": "Read an estimate",
        "description": "Returns the estimate directly, not wrapped in an envelope object (unlike the list endpoint's estimates key). Responds 403 when the caller is not a member of the estimate's workshop and 404 when no such estimate exists.",
        "parameters": [
          {
            "name": "estimateId",
            "in": "path",
            "required": true,
            "description": "Estimate id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Estimate"
                },
                "example": {
                  "id": "estimate-1",
                  "workshopId": "workshop-1",
                  "name": "Bespoke chair estimate",
                  "recipientName": "Anna Kowalska",
                  "createdAt": "2026-08-01T09:00:00.000Z",
                  "updatedAt": "2026-08-10T14:30:00.000Z",
                  "markupPercent": 10,
                  "discountPercent": 0,
                  "lineItems": [
                    {
                      "id": "line-item-1",
                      "label": "Oak plank, 2m",
                      "unit": "sqm",
                      "unitPrice": 120,
                      "quantity": 4
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not found"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      },
      "patch": {
        "operationId": "patchEstimate",
        "tags": [
          "Estimates"
        ],
        "summary": "Update an estimate's name, recipient, markup or discount",
        "description": "Updates any subset of name, recipientName, markupPercent and discountPercent; unlike the order equivalent, an empty body is accepted, it only bumps updatedAt. The body is strict, an unknown field is a 400. A provided name must be non-blank after trimming; a provided recipientName may be an empty string, which clears the field entirely rather than storing blank text. Line items are not editable through this endpoint, see the line-item endpoints below. None of these fields counts as a new revision, call confirm to snapshot the result. Responds 403 when the caller is not a member of the estimate's workshop and 404 when no such estimate exists.",
        "parameters": [
          {
            "name": "estimateId",
            "in": "path",
            "required": true,
            "description": "Estimate id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1
                  },
                  "recipientName": {
                    "type": "string"
                  },
                  "markupPercent": {
                    "type": "number",
                    "minimum": 0
                  },
                  "discountPercent": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100
                  }
                },
                "additionalProperties": false
              },
              "example": {
                "markupPercent": 15
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Estimate"
                },
                "example": {
                  "id": "estimate-1",
                  "workshopId": "workshop-1",
                  "name": "Bespoke chair estimate",
                  "recipientName": "Anna Kowalska",
                  "createdAt": "2026-08-01T09:00:00.000Z",
                  "updatedAt": "2026-08-10T14:30:00.000Z",
                  "markupPercent": 15,
                  "discountPercent": 0,
                  "lineItems": [
                    {
                      "id": "line-item-1",
                      "label": "Oak plank, 2m",
                      "unit": "sqm",
                      "unitPrice": 120,
                      "quantity": 4
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "The request body does not match the schema",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not found"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      },
      "delete": {
        "operationId": "deleteEstimate",
        "tags": [
          "Estimates"
        ],
        "summary": "Delete an estimate",
        "description": "Permanently deletes the estimate, its revision history, its PDF exports and its hand-off history, and frees the workshop's file storage quota used by its stored PDFs; the PDF files themselves are deleted too. Responds with an empty body on success. Responds 403 when the caller is not a member of the estimate's workshop and 404 when no such estimate exists.",
        "parameters": [
          {
            "name": "estimateId",
            "in": "path",
            "required": true,
            "description": "Estimate id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Not authenticated"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not found"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      }
    },
    "/estimates/{estimateId}/confirm": {
      "post": {
        "operationId": "postEstimateConfirm",
        "tags": [
          "Estimates"
        ],
        "summary": "Snapshot an estimate's current content as a new revision",
        "description": "Estimate creation and duplication already write revision 1; confirm snapshots the estimate's current name, markup, discount and line items as the next revisionNumber. After writing it, revisions beyond the workshop's plan cap (GET /workshops/{workshopId}/entitlements's revisions.limitPerEstimate, null on Pro) are pruned silently, oldest first - there is no error response for exceeding the cap, the history is simply trimmed. The cap is only enforced at confirm time, so a plan downgrade does not retroactively prune existing history until the next confirm. Responds 403 when the caller is not a member of the estimate's workshop and 404 when no such estimate exists.",
        "parameters": [
          {
            "name": "estimateId",
            "in": "path",
            "required": true,
            "description": "Estimate id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EstimateConfirmation"
                },
                "example": {
                  "estimate": {
                    "id": "estimate-1",
                    "workshopId": "workshop-1",
                    "name": "Bespoke chair estimate",
                    "recipientName": "Anna Kowalska",
                    "createdAt": "2026-08-01T09:00:00.000Z",
                    "updatedAt": "2026-08-10T14:30:00.000Z",
                    "markupPercent": 10,
                    "discountPercent": 0,
                    "lineItems": [
                      {
                        "id": "line-item-1",
                        "label": "Oak plank, 2m",
                        "unit": "sqm",
                        "unitPrice": 120,
                        "quantity": 4
                      }
                    ]
                  },
                  "revision": {
                    "id": "revision-1",
                    "revisionNumber": 1,
                    "createdAt": "2026-08-01T09:00:00.000Z",
                    "createdByUid": "uid-1",
                    "name": "Bespoke chair estimate",
                    "markupPercent": 10,
                    "discountPercent": 0,
                    "lineItems": [
                      {
                        "id": "line-item-1",
                        "label": "Oak plank, 2m",
                        "unit": "sqm",
                        "unitPrice": 120,
                        "quantity": 4
                      }
                    ]
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not found"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      }
    },
    "/estimates/{estimateId}/revisions": {
      "get": {
        "operationId": "getEstimateRevisions",
        "tags": [
          "Estimates"
        ],
        "summary": "List an estimate's revisions",
        "description": "Returns every stored revision, newest revisionNumber first, already trimmed to the workshop's plan cap (see the confirm endpoint). Responds 403 when the caller is not a member of the estimate's workshop and 404 when no such estimate exists.",
        "parameters": [
          {
            "name": "estimateId",
            "in": "path",
            "required": true,
            "description": "Estimate id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EstimateRevisionsList"
                },
                "example": {
                  "revisions": [
                    {
                      "id": "revision-1",
                      "revisionNumber": 1,
                      "createdAt": "2026-08-01T09:00:00.000Z",
                      "createdByUid": "uid-1",
                      "name": "Bespoke chair estimate",
                      "markupPercent": 10,
                      "discountPercent": 0,
                      "lineItems": [
                        {
                          "id": "line-item-1",
                          "label": "Oak plank, 2m",
                          "unit": "sqm",
                          "unitPrice": 120,
                          "quantity": 4
                        }
                      ]
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not found"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      }
    },
    "/estimates/{estimateId}/duplicate": {
      "post": {
        "operationId": "postEstimateDuplicate",
        "tags": [
          "Estimates"
        ],
        "summary": "Duplicate an estimate",
        "description": "Creates a new estimate copying name (suffixed with \" (copy)\"), recipientName, markupPercent, discountPercent and every line item, each assigned a fresh id; the source's revision history is not copied, the duplicate gets its own fresh revision 1 immediately, same as a plain create. Responds 403 when the caller is not a member of the estimate's workshop and 404 when no such estimate exists.",
        "parameters": [
          {
            "name": "estimateId",
            "in": "path",
            "required": true,
            "description": "Estimate id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Estimate"
                },
                "example": {
                  "id": "estimate-2",
                  "workshopId": "workshop-1",
                  "name": "Bespoke chair estimate (copy)",
                  "recipientName": "Anna Kowalska",
                  "createdAt": "2026-08-01T09:00:00.000Z",
                  "updatedAt": "2026-08-10T14:30:00.000Z",
                  "markupPercent": 10,
                  "discountPercent": 0,
                  "lineItems": [
                    {
                      "id": "line-item-1",
                      "label": "Oak plank, 2m",
                      "unit": "sqm",
                      "unitPrice": 120,
                      "quantity": 4
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not found"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      }
    },
    "/estimates/{estimateId}/line-items": {
      "post": {
        "operationId": "postEstimateLineItem",
        "tags": [
          "Estimates"
        ],
        "summary": "Add a line item to an estimate",
        "description": "Appends one line item, assigned a fresh id, and returns the whole updated estimate, not just the new item. The body is not strict, an unknown field is silently dropped rather than rejected. sourceButtonId (a grid button id) is stored as given, not validated against the workshop's grids. Responds 403 when the caller is not a member of the estimate's workshop and 404 when no such estimate exists.",
        "parameters": [
          {
            "name": "estimateId",
            "in": "path",
            "required": true,
            "description": "Estimate id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "label": {
                    "type": "string",
                    "minLength": 1
                  },
                  "unit": {
                    "$ref": "#/components/schemas/LineItemUnit"
                  },
                  "unitPrice": {
                    "type": "number",
                    "minimum": 0
                  },
                  "quantity": {
                    "type": "number",
                    "exclusiveMinimum": 0
                  },
                  "sourceButtonId": {
                    "type": "string"
                  }
                },
                "required": [
                  "label",
                  "unit",
                  "unitPrice",
                  "quantity"
                ]
              },
              "example": {
                "label": "Oak plank, 2m",
                "unit": "sqm",
                "unitPrice": 120,
                "quantity": 4
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Estimate"
                },
                "example": {
                  "id": "estimate-1",
                  "workshopId": "workshop-1",
                  "name": "Bespoke chair estimate",
                  "recipientName": "Anna Kowalska",
                  "createdAt": "2026-08-01T09:00:00.000Z",
                  "updatedAt": "2026-08-10T14:30:00.000Z",
                  "markupPercent": 10,
                  "discountPercent": 0,
                  "lineItems": [
                    {
                      "id": "line-item-1",
                      "label": "Oak plank, 2m",
                      "unit": "sqm",
                      "unitPrice": 120,
                      "quantity": 4
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "The request body does not match the schema",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not found"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      }
    },
    "/estimates/{estimateId}/line-items/{lineItemId}": {
      "patch": {
        "operationId": "patchEstimateLineItem",
        "tags": [
          "Estimates"
        ],
        "summary": "Update a line item",
        "description": "Updates any subset of a line item's label, unit, unitPrice, quantity and sourceButtonId, and returns the whole updated estimate, not just the changed item. The body is strict, an unknown field is a 400. Responds 403 when the caller is not a member of the estimate's workshop and 404 both when no such estimate exists and when the estimate exists but has no line item with this id - the two cases are indistinguishable in the response.",
        "parameters": [
          {
            "name": "estimateId",
            "in": "path",
            "required": true,
            "description": "Estimate id",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "lineItemId",
            "in": "path",
            "required": true,
            "description": "Line item id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "label": {
                    "type": "string",
                    "minLength": 1
                  },
                  "unit": {
                    "$ref": "#/components/schemas/LineItemUnit"
                  },
                  "unitPrice": {
                    "type": "number",
                    "minimum": 0
                  },
                  "quantity": {
                    "type": "number",
                    "exclusiveMinimum": 0
                  },
                  "sourceButtonId": {
                    "type": "string"
                  }
                },
                "additionalProperties": false
              },
              "example": {
                "quantity": 3
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Estimate"
                },
                "example": {
                  "id": "estimate-1",
                  "workshopId": "workshop-1",
                  "name": "Bespoke chair estimate",
                  "recipientName": "Anna Kowalska",
                  "createdAt": "2026-08-01T09:00:00.000Z",
                  "updatedAt": "2026-08-10T14:30:00.000Z",
                  "markupPercent": 10,
                  "discountPercent": 0,
                  "lineItems": [
                    {
                      "id": "line-item-1",
                      "label": "Oak plank, 2m",
                      "unit": "sqm",
                      "unitPrice": 120,
                      "quantity": 4
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "The request body does not match the schema",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not found"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      },
      "delete": {
        "operationId": "deleteEstimateLineItem",
        "tags": [
          "Estimates"
        ],
        "summary": "Remove a line item",
        "description": "Removes one line item and returns the whole updated estimate, not an empty body (unlike deleting the estimate itself). Responds 403 when the caller is not a member of the estimate's workshop and 404 both when no such estimate exists and when the estimate exists but has no line item with this id - the two cases are indistinguishable in the response.",
        "parameters": [
          {
            "name": "estimateId",
            "in": "path",
            "required": true,
            "description": "Estimate id",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "lineItemId",
            "in": "path",
            "required": true,
            "description": "Line item id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Estimate"
                },
                "example": {
                  "id": "estimate-1",
                  "workshopId": "workshop-1",
                  "name": "Bespoke chair estimate",
                  "recipientName": "Anna Kowalska",
                  "createdAt": "2026-08-01T09:00:00.000Z",
                  "updatedAt": "2026-08-10T14:30:00.000Z",
                  "markupPercent": 10,
                  "discountPercent": 0,
                  "lineItems": [
                    {
                      "id": "line-item-1",
                      "label": "Oak plank, 2m",
                      "unit": "sqm",
                      "unitPrice": 120,
                      "quantity": 4
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not found"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      }
    },
    "/estimates/{estimateId}/pdf-exports": {
      "post": {
        "operationId": "postEstimatePdfExport",
        "tags": [
          "Estimate exports"
        ],
        "summary": "Generate a PDF export of an estimate",
        "description": "Renders the estimate's current content (not a specific revision) into a PDF, stores it as a file scoped to the estimate and records its metadata; every toggle and locale defaults server-side when omitted, see PdfExportOptions and DocumentLocale. The body is strict, an unknown field is a 400. Responds 201 with the new PdfExport and a Location header pointing to GET .../pdf-exports/{pdfExportId}. Responds 403 when the caller is not a member of the estimate's workshop and 404 when no such estimate exists. Responds 413 with an empty body when generating this PDF would exceed the workshop's plan file storage quota.",
        "parameters": [
          {
            "name": "estimateId",
            "in": "path",
            "required": true,
            "description": "Estimate id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "breakdown": {
                    "default": true,
                    "description": "Include the itemized line-item breakdown table.",
                    "type": "boolean"
                  },
                  "showMarkup": {
                    "default": false,
                    "description": "Show the markup percentage and amount.",
                    "type": "boolean"
                  },
                  "showDiscount": {
                    "default": false,
                    "description": "Show the discount percentage and amount.",
                    "type": "boolean"
                  },
                  "showRecipient": {
                    "default": true,
                    "description": "Show the recipient's name.",
                    "type": "boolean"
                  },
                  "showBrandMark": {
                    "default": true,
                    "description": "Show Mercastra's brand mark. A workshop whose plan cannot disable it (see entitlements) has this forced back to true server-side regardless of what is sent.",
                    "type": "boolean"
                  },
                  "locale": {
                    "default": "pl",
                    "description": "Defaults to Polish.",
                    "$ref": "#/components/schemas/DocumentLocale"
                  },
                  "preliminary": {
                    "default": false,
                    "description": "Marks the PDF as a preliminary estimate, changing its disclaimer text.",
                    "type": "boolean"
                  }
                },
                "additionalProperties": false
              },
              "example": {
                "locale": "en",
                "showMarkup": true
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PdfExport"
                },
                "example": {
                  "id": "pdf-export-1",
                  "revisionNumber": 1,
                  "toggles": {
                    "breakdown": true,
                    "showMarkup": true,
                    "showDiscount": false,
                    "showRecipient": true,
                    "showBrandMark": true
                  },
                  "generatedAt": "2026-08-10T14:35:00.000Z",
                  "createdByUid": "uid-1",
                  "locale": "en",
                  "preliminary": false
                }
              }
            }
          },
          "400": {
            "description": "The request body does not match the schema",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not found"
          },
          "413": {
            "description": "Generating this PDF would exceed the workshop's plan file storage quota"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      },
      "get": {
        "operationId": "getEstimatePdfExports",
        "tags": [
          "Estimate exports"
        ],
        "summary": "List an estimate's PDF exports",
        "description": "Returns every stored pdf export's metadata, newest generatedAt first. Responds 403 when the caller is not a member of the estimate's workshop and 404 when no such estimate exists.",
        "parameters": [
          {
            "name": "estimateId",
            "in": "path",
            "required": true,
            "description": "Estimate id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PdfExportsList"
                },
                "example": {
                  "pdfExports": [
                    {
                      "id": "pdf-export-1",
                      "revisionNumber": 1,
                      "toggles": {
                        "breakdown": true,
                        "showMarkup": true,
                        "showDiscount": false,
                        "showRecipient": true,
                        "showBrandMark": true
                      },
                      "generatedAt": "2026-08-10T14:35:00.000Z",
                      "createdByUid": "uid-1",
                      "locale": "en",
                      "preliminary": false
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not found"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      }
    },
    "/estimates/{estimateId}/pdf-exports/{pdfExportId}": {
      "get": {
        "operationId": "getEstimatePdfExportFile",
        "tags": [
          "Estimate exports"
        ],
        "summary": "Download a PDF export's bytes",
        "description": "Streams the stored PDF with Content-Disposition: attachment and a filename derived from the export's locale (estimate-<id>.pdf for en, wycena-<id>.pdf for pl). Responds 403 when the caller is not a member of the estimate's workshop and 404 both when no such estimate or pdf export exists and when the pdf export id belongs to a different estimate - the three cases are indistinguishable in the response.",
        "parameters": [
          {
            "name": "estimateId",
            "in": "path",
            "required": true,
            "description": "Estimate id",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "pdfExportId",
            "in": "path",
            "required": true,
            "description": "Pdf export id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not found"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      },
      "delete": {
        "operationId": "deleteEstimatePdfExport",
        "tags": [
          "Estimate exports"
        ],
        "summary": "Delete a PDF export",
        "description": "Permanently deletes the pdf export's metadata record and its stored bytes, freeing the workshop's file storage quota it used. Responds 200 with an empty body on success, not 204 - a hand-maintained doc has previously claimed 204, that was wrong, the route calls res.status(200).send(). Responds 403 when the caller is not a member of the estimate's workshop and 404 both when no such estimate or pdf export exists and when the pdf export id belongs to a different estimate - the three cases are indistinguishable in the response.",
        "parameters": [
          {
            "name": "estimateId",
            "in": "path",
            "required": true,
            "description": "Estimate id",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "pdfExportId",
            "in": "path",
            "required": true,
            "description": "Pdf export id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Not authenticated"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not found"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      }
    },
    "/estimates/{estimateId}/handoffs": {
      "post": {
        "operationId": "postEstimateHandoff",
        "tags": [
          "Estimate exports"
        ],
        "summary": "Record that an estimate was handed off to its recipient",
        "description": "Records that a hand-off happened; it does not send anything itself, the caller performs the copy, share or open action and reports it here afterward. None of kind, tone or aiDrafted is verified against anything that actually happened, they are trusted as given. The body is strict, an unknown field is a 400. When pdfExportId is given it must identify an existing pdf export on this estimate; if it does not, this also responds 400, but with an empty body, not the ValidationError JSON a schema violation sends - the two 400 causes are otherwise indistinguishable from the status code alone. Responds 403 when the caller is not a member of the estimate's workshop and 404 when no such estimate exists.",
        "parameters": [
          {
            "name": "estimateId",
            "in": "path",
            "required": true,
            "description": "Estimate id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "kind": {
                    "$ref": "#/components/schemas/HandoffKind"
                  },
                  "locale": {
                    "$ref": "#/components/schemas/DocumentLocale"
                  },
                  "preliminary": {
                    "type": "boolean"
                  },
                  "tone": {
                    "$ref": "#/components/schemas/MessageTone"
                  },
                  "aiDrafted": {
                    "type": "boolean"
                  },
                  "pdfExportId": {
                    "description": "An existing pdf export to associate with this hand-off, when one was involved.",
                    "type": "string",
                    "minLength": 1
                  }
                },
                "required": [
                  "kind",
                  "locale",
                  "preliminary",
                  "tone",
                  "aiDrafted"
                ],
                "additionalProperties": false
              },
              "example": {
                "kind": "pdf-shared",
                "locale": "pl",
                "preliminary": false,
                "tone": "friendly",
                "aiDrafted": false,
                "pdfExportId": "pdf-export-1"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Handoff"
                },
                "example": {
                  "id": "handoff-1",
                  "kind": "pdf-shared",
                  "locale": "pl",
                  "preliminary": false,
                  "tone": "friendly",
                  "aiDrafted": false,
                  "revisionNumber": 1,
                  "createdAt": "2026-08-10T14:40:00.000Z",
                  "createdByUid": "uid-1",
                  "pdfExportId": "pdf-export-1"
                }
              }
            }
          },
          "400": {
            "description": "Either the body fails schema validation (ValidationError JSON) or pdfExportId does not identify a pdf export belonging to this estimate (empty body) - see this endpoint's description.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not found"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      },
      "get": {
        "operationId": "getEstimateHandoffs",
        "tags": [
          "Estimate exports"
        ],
        "summary": "List an estimate's hand-offs",
        "description": "Returns every recorded hand-off, newest createdAt first. Responds 403 when the caller is not a member of the estimate's workshop and 404 when no such estimate exists.",
        "parameters": [
          {
            "name": "estimateId",
            "in": "path",
            "required": true,
            "description": "Estimate id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HandoffsList"
                },
                "example": {
                  "handoffs": [
                    {
                      "id": "handoff-1",
                      "kind": "pdf-shared",
                      "locale": "pl",
                      "preliminary": false,
                      "tone": "friendly",
                      "aiDrafted": false,
                      "revisionNumber": 1,
                      "createdAt": "2026-08-10T14:40:00.000Z",
                      "createdByUid": "uid-1",
                      "pdfExportId": "pdf-export-1"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not found"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      }
    },
    "/workshops/{workshopId}/grids": {
      "get": {
        "operationId": "getWorkshopGrids",
        "tags": [
          "Grids"
        ],
        "summary": "List a workshop's grids",
        "description": "Returns every grid belonging to the workshop. Responds 403 when the caller is not a member of the workshop; there is no 404 for an unknown workshop id, a non-member sees 403 either way.",
        "parameters": [
          {
            "name": "workshopId",
            "in": "path",
            "required": true,
            "description": "Workshop id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GridsList"
                },
                "example": {
                  "grids": [
                    {
                      "id": "grid-1",
                      "workshopId": "workshop-1",
                      "name": "Standard pricing",
                      "iconSize": "medium",
                      "createdAt": "2026-08-01T09:00:00.000Z",
                      "updatedAt": "2026-08-10T14:30:00.000Z",
                      "buttons": [
                        {
                          "id": "button-1",
                          "emoji": "🛠️",
                          "label": "Labor",
                          "unit": "hour",
                          "unitPrice": 80
                        }
                      ]
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated"
          },
          "403": {
            "description": "Forbidden"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      },
      "post": {
        "operationId": "postWorkshopGrid",
        "tags": [
          "Grids"
        ],
        "summary": "Create a grid in a workshop",
        "description": "Creates a new grid. name defaults to \"Untitled grid\" when omitted or blank after trimming; iconSize defaults to medium and buttons default to none, each submitted button is assigned a fresh id. The body is not strict, an unknown field is silently dropped rather than rejected. Responds 403 when the caller is not a member of the workshop and 409 with an empty body when the workshop already has 10 grids, the per-workshop cap; there is no 404 for an unknown workshop id, a non-member sees 403 either way.",
        "parameters": [
          {
            "name": "workshopId",
            "in": "path",
            "required": true,
            "description": "Workshop id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "iconSize": {
                    "$ref": "#/components/schemas/GridIconSize"
                  },
                  "buttons": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "emoji": {
                          "$ref": "#/components/schemas/Emoji"
                        },
                        "label": {
                          "type": "string",
                          "minLength": 1
                        },
                        "unit": {
                          "$ref": "#/components/schemas/LineItemUnit"
                        },
                        "unitPrice": {
                          "type": "number",
                          "minimum": 0
                        },
                        "backgroundColor": {
                          "$ref": "#/components/schemas/HexColor"
                        },
                        "iconFileId": {
                          "type": "string",
                          "minLength": 1
                        }
                      },
                      "required": [
                        "emoji",
                        "label",
                        "unit",
                        "unitPrice"
                      ]
                    }
                  }
                }
              },
              "example": {
                "name": "Standard pricing"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Grid"
                },
                "example": {
                  "id": "grid-1",
                  "workshopId": "workshop-1",
                  "name": "Standard pricing",
                  "iconSize": "medium",
                  "createdAt": "2026-08-01T09:00:00.000Z",
                  "updatedAt": "2026-08-10T14:30:00.000Z",
                  "buttons": [
                    {
                      "id": "button-1",
                      "emoji": "🛠️",
                      "label": "Labor",
                      "unit": "hour",
                      "unitPrice": 80
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "The request body does not match the schema",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated"
          },
          "403": {
            "description": "Forbidden"
          },
          "409": {
            "description": "Conflict"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      }
    },
    "/grids/{gridId}": {
      "get": {
        "operationId": "getGrid",
        "tags": [
          "Grids"
        ],
        "summary": "Read a grid",
        "description": "Responds 403 when the caller is not a member of the grid's workshop and 404 when no such grid exists.",
        "parameters": [
          {
            "name": "gridId",
            "in": "path",
            "required": true,
            "description": "Grid id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Grid"
                },
                "example": {
                  "id": "grid-1",
                  "workshopId": "workshop-1",
                  "name": "Standard pricing",
                  "iconSize": "medium",
                  "createdAt": "2026-08-01T09:00:00.000Z",
                  "updatedAt": "2026-08-10T14:30:00.000Z",
                  "buttons": [
                    {
                      "id": "button-1",
                      "emoji": "🛠️",
                      "label": "Labor",
                      "unit": "hour",
                      "unitPrice": 80
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not found"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      },
      "patch": {
        "operationId": "patchGrid",
        "tags": [
          "Grids"
        ],
        "summary": "Rename a grid or change its icon size",
        "description": "Updates any subset of name and iconSize; the body is strict, an unknown field is a 400. Neither field can be cleared, only replaced with a new value. Buttons are not editable through this endpoint, see the button endpoints below. Responds 403 when the caller is not a member of the grid's workshop and 404 when no such grid exists.",
        "parameters": [
          {
            "name": "gridId",
            "in": "path",
            "required": true,
            "description": "Grid id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1
                  },
                  "iconSize": {
                    "$ref": "#/components/schemas/GridIconSize"
                  }
                },
                "additionalProperties": false
              },
              "example": {
                "name": "Standard pricing"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Grid"
                },
                "example": {
                  "id": "grid-1",
                  "workshopId": "workshop-1",
                  "name": "Standard pricing",
                  "iconSize": "medium",
                  "createdAt": "2026-08-01T09:00:00.000Z",
                  "updatedAt": "2026-08-10T14:30:00.000Z",
                  "buttons": [
                    {
                      "id": "button-1",
                      "emoji": "🛠️",
                      "label": "Labor",
                      "unit": "hour",
                      "unitPrice": 80
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "The request body does not match the schema",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not found"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      },
      "delete": {
        "operationId": "deleteGrid",
        "tags": [
          "Grids"
        ],
        "summary": "Delete a grid",
        "description": "Permanently deletes the grid and its buttons. Responds with an empty body on success. Responds 403 when the caller is not a member of the grid's workshop and 404 when no such grid exists.",
        "parameters": [
          {
            "name": "gridId",
            "in": "path",
            "required": true,
            "description": "Grid id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Not authenticated"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not found"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      }
    },
    "/grids/{gridId}/duplicate": {
      "post": {
        "operationId": "postGridDuplicate",
        "tags": [
          "Grids"
        ],
        "summary": "Duplicate a grid",
        "description": "Creates a new grid copying name (suffixed with \" (copy)\"), iconSize and every button, each assigned a fresh id; everything else about a button is copied verbatim, including its iconFileId. Responds 403 when the caller is not a member of the grid's workshop, 404 when no such grid exists, and 409 with an empty body when the workshop already has 10 grids, the same per-workshop cap as creating one, checked against the source grid's workshop.",
        "parameters": [
          {
            "name": "gridId",
            "in": "path",
            "required": true,
            "description": "Grid id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Grid"
                },
                "example": {
                  "id": "grid-2",
                  "workshopId": "workshop-1",
                  "name": "Standard pricing (copy)",
                  "iconSize": "medium",
                  "createdAt": "2026-08-01T09:00:00.000Z",
                  "updatedAt": "2026-08-10T14:30:00.000Z",
                  "buttons": [
                    {
                      "id": "button-1",
                      "emoji": "🛠️",
                      "label": "Labor",
                      "unit": "hour",
                      "unitPrice": 80
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not found"
          },
          "409": {
            "description": "Conflict"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      }
    },
    "/grids/{gridId}/buttons": {
      "post": {
        "operationId": "postGridButton",
        "tags": [
          "Grids"
        ],
        "summary": "Add a button to a grid",
        "description": "Appends one button, assigned a fresh id, and returns the whole updated grid, not just the new button. The body is not strict, an unknown field is silently dropped rather than rejected. There is no per-grid button count cap, only the per-workshop grid cap above. Responds 403 when the caller is not a member of the grid's workshop and 404 when no such grid exists. Responds 400 when the body fails schema validation, in the usual ValidationError shape, or - with an empty body instead, not ValidationError - when iconFileId is given but does not reference a file uploaded with category grid_tile_icon scoped to this grid's workshop.",
        "parameters": [
          {
            "name": "gridId",
            "in": "path",
            "required": true,
            "description": "Grid id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "emoji": {
                    "$ref": "#/components/schemas/Emoji"
                  },
                  "label": {
                    "type": "string",
                    "minLength": 1
                  },
                  "unit": {
                    "$ref": "#/components/schemas/LineItemUnit"
                  },
                  "unitPrice": {
                    "type": "number",
                    "minimum": 0
                  },
                  "backgroundColor": {
                    "$ref": "#/components/schemas/HexColor"
                  },
                  "iconFileId": {
                    "type": "string",
                    "minLength": 1
                  }
                },
                "required": [
                  "emoji",
                  "label",
                  "unit",
                  "unitPrice"
                ]
              },
              "example": {
                "emoji": "🛠️",
                "label": "Labor",
                "unit": "hour",
                "unitPrice": 80
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Grid"
                },
                "example": {
                  "id": "grid-1",
                  "workshopId": "workshop-1",
                  "name": "Standard pricing",
                  "iconSize": "medium",
                  "createdAt": "2026-08-01T09:00:00.000Z",
                  "updatedAt": "2026-08-10T14:30:00.000Z",
                  "buttons": [
                    {
                      "id": "button-1",
                      "emoji": "🛠️",
                      "label": "Labor",
                      "unit": "hour",
                      "unitPrice": 80
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "The body fails schema validation (ValidationError shape), or iconFileId does not reference a valid, unused grid_tile_icon file (empty body)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not found"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      }
    },
    "/grids/{gridId}/buttons/{buttonId}": {
      "patch": {
        "operationId": "patchGridButton",
        "tags": [
          "Grids"
        ],
        "summary": "Update a button",
        "description": "Updates any subset of a button's emoji, label, unit, unitPrice, backgroundColor and iconFileId, and returns the whole updated grid, not just the changed button. The body is strict, an unknown field is a 400. backgroundColor and iconFileId are independently clearable: omit a key to leave it untouched, send it as null to remove the button's existing value, or send a new value to replace it - this is distinct from the other three fields, which have no null-clearing behavior since a button always has an emoji, a label and a unit. Sending iconFileId as null skips the icon-validity check below. Responds 403 when the caller is not a member of the grid's workshop and 404 both when no such grid exists and when the grid exists but has no button with this id - the two cases are indistinguishable in the response. Responds 400 when the body fails schema validation, in the usual ValidationError shape, or - with an empty body instead, not ValidationError - when iconFileId is given as a non-null value that does not reference a file uploaded with category grid_tile_icon scoped to this grid's workshop.",
        "parameters": [
          {
            "name": "gridId",
            "in": "path",
            "required": true,
            "description": "Grid id",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "buttonId",
            "in": "path",
            "required": true,
            "description": "Button id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "emoji": {
                    "$ref": "#/components/schemas/Emoji"
                  },
                  "label": {
                    "type": "string",
                    "minLength": 1
                  },
                  "unit": {
                    "$ref": "#/components/schemas/LineItemUnit"
                  },
                  "unitPrice": {
                    "type": "number",
                    "minimum": 0
                  },
                  "backgroundColor": {
                    "anyOf": [
                      {
                        "$ref": "#/components/schemas/HexColor"
                      },
                      {
                        "type": "null"
                      }
                    ]
                  },
                  "iconFileId": {
                    "anyOf": [
                      {
                        "type": "string",
                        "minLength": 1
                      },
                      {
                        "type": "null"
                      }
                    ]
                  }
                },
                "additionalProperties": false
              },
              "example": {
                "unitPrice": 95
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Grid"
                },
                "example": {
                  "id": "grid-1",
                  "workshopId": "workshop-1",
                  "name": "Standard pricing",
                  "iconSize": "medium",
                  "createdAt": "2026-08-01T09:00:00.000Z",
                  "updatedAt": "2026-08-10T14:30:00.000Z",
                  "buttons": [
                    {
                      "id": "button-1",
                      "emoji": "🛠️",
                      "label": "Labor",
                      "unit": "hour",
                      "unitPrice": 80
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "The body fails schema validation (ValidationError shape), or a non-null iconFileId does not reference a valid grid_tile_icon file (empty body)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not found"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      },
      "delete": {
        "operationId": "deleteGridButton",
        "tags": [
          "Grids"
        ],
        "summary": "Remove a button",
        "description": "Removes one button and returns the whole updated grid, not an empty body (unlike deleting the grid itself). Responds 403 when the caller is not a member of the grid's workshop and 404 both when no such grid exists and when the grid exists but has no button with this id - the two cases are indistinguishable in the response.",
        "parameters": [
          {
            "name": "gridId",
            "in": "path",
            "required": true,
            "description": "Grid id",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "buttonId",
            "in": "path",
            "required": true,
            "description": "Button id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Grid"
                },
                "example": {
                  "id": "grid-1",
                  "workshopId": "workshop-1",
                  "name": "Standard pricing",
                  "iconSize": "medium",
                  "createdAt": "2026-08-01T09:00:00.000Z",
                  "updatedAt": "2026-08-10T14:30:00.000Z",
                  "buttons": [
                    {
                      "id": "button-1",
                      "emoji": "🛠️",
                      "label": "Labor",
                      "unit": "hour",
                      "unitPrice": 80
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not found"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      }
    },
    "/workshops/{workshopId}/grid-icons/{fileId}": {
      "delete": {
        "operationId": "deleteWorkshopGridIcon",
        "tags": [
          "Grids"
        ],
        "summary": "Delete a grid button icon",
        "description": "Deletes a file uploaded with category grid_tile_icon, scoped to this workshop. When one or more grid buttons in the workshop still reference the icon by iconFileId, the default behavior is to respond 409 with the ids of every grid using it and delete nothing; pass detach=true to instead clear iconFileId from every referencing button first and then delete the file. detach is compared to the literal string \"true\"; any other value, including absent, is treated as false, there is no 400 for an invalid value. Responds 403 when the caller is not a member of the workshop and 404 when fileId does not reference a grid_tile_icon file scoped to this workshop.",
        "parameters": [
          {
            "name": "workshopId",
            "in": "path",
            "required": true,
            "description": "Workshop id",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "fileId",
            "in": "path",
            "required": true,
            "description": "File id",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "detach",
            "in": "query",
            "required": false,
            "description": "Pass the literal string \"true\" to detach and delete; any other value behaves as false",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Not authenticated"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not found"
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GridIconInUse"
                },
                "example": {
                  "gridIds": [
                    "grid-1"
                  ]
                }
              }
            }
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      }
    },
    "/predefined-items": {
      "get": {
        "operationId": "getPredefinedItems",
        "tags": [
          "Catalog"
        ],
        "summary": "List the catalog of predefined grid items, domains and templates",
        "description": "Reference data for building grids by script, the same catalog POST /workshops/{workshopId}/grids/ai-draft draws on: items are grouped by domain and by template, each by slug or id match with no referential enforcement, so a template's itemIds or a domain's slug may point at something absent from this response if the catalog changes. Not workshop-scoped, the same response for every caller. In production this returns on the order of 200 items across 16 domains and 27 templates; the example below is a truncated slice.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PredefinedItemsCatalog"
                },
                "example": {
                  "items": [
                    {
                      "id": "labor",
                      "emoji": "🔧",
                      "label": {
                        "en": "Labor",
                        "pl": "Robocizna"
                      },
                      "unit": "hour",
                      "tags": [
                        "labor"
                      ]
                    }
                  ],
                  "domains": [
                    {
                      "slug": "general",
                      "label": {
                        "en": "General",
                        "pl": "Ogólne"
                      }
                    }
                  ],
                  "templates": [
                    {
                      "slug": "common-fees-and-add-ons",
                      "domainSlug": "general",
                      "name": {
                        "en": "Common Fees & Add-ons",
                        "pl": "Typowe opłaty i dodatki"
                      },
                      "description": {
                        "en": "Labor, callouts, deposits and delivery, ready to mix into any grid.",
                        "pl": "Robocizna, dojazdy, zaliczki i dostawa - gotowe, by dodać do dowolnego cennika."
                      },
                      "itemIds": [
                        "labor"
                      ]
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated"
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "x-stability": "experimental"
      }
    }
  },
  "components": {
    "schemas": {
      "AuthenticateResponse": {
        "type": "object",
        "properties": {
          "user": {
            "$ref": "#/components/schemas/AuthenticatedUser"
          }
        },
        "required": [
          "user"
        ],
        "additionalProperties": false
      },
      "AuthenticatedUser": {
        "type": "object",
        "properties": {
          "uid": {
            "type": "string"
          },
          "email": {
            "type": "string",
            "format": "email",
            "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
          },
          "name": {
            "type": "string"
          },
          "locale": {
            "$ref": "#/components/schemas/UserLocale"
          }
        },
        "required": [
          "uid",
          "email",
          "name",
          "locale"
        ],
        "additionalProperties": false,
        "description": "The signed-in account, as returned by the endpoint that grants the session."
      },
      "UserLocale": {
        "type": "string",
        "enum": [
          "en",
          "pl"
        ],
        "description": "The account's UI language preference."
      },
      "MeProfile": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "email": {
            "type": "string",
            "format": "email",
            "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
          },
          "locale": {
            "$ref": "#/components/schemas/UserLocale"
          },
          "signInMethods": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SignInMethod"
            }
          }
        },
        "required": [
          "name",
          "email",
          "locale",
          "signInMethods"
        ],
        "additionalProperties": false,
        "description": "The signed-in account's own profile."
      },
      "SignInMethod": {
        "type": "string",
        "enum": [
          "password",
          "google"
        ],
        "description": "A way the account can authenticate: a password, or a linked OAuth provider."
      },
      "ValidationError": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "const": "fail"
          },
          "message": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "path": {
                  "type": "array",
                  "items": {
                    "type": [
                      "string",
                      "number"
                    ]
                  }
                },
                "message": {
                  "type": "string"
                }
              },
              "required": [
                "path",
                "message"
              ],
              "additionalProperties": {}
            }
          }
        },
        "required": [
          "status",
          "message"
        ],
        "additionalProperties": false,
        "description": "Sent with status 400 when the request body does not match the endpoint's schema. Each issue names the offending path and says what is wrong with it."
      },
      "AccountDataExport": {
        "type": "object",
        "properties": {
          "user": {
            "$ref": "#/components/schemas/AccountDataExportUser"
          },
          "workshops": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AccountDataExportWorkshopEntry"
            }
          },
          "feedback": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AccountDataExportFeedback"
            }
          },
          "authProviders": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AccountDataExportAuthProvider"
            }
          }
        },
        "required": [
          "user",
          "workshops",
          "feedback",
          "authProviders"
        ],
        "additionalProperties": false,
        "description": "Everything Mercastra stores about the account and the workshops it belongs to, for portability. Workshops the account is only a member of (not the owner) are included in the same list as owned ones."
      },
      "AccountDataExportUser": {
        "type": "object",
        "properties": {
          "uid": {
            "type": "string"
          },
          "email": {
            "type": "string",
            "format": "email",
            "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
          },
          "name": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "active",
              "suspended",
              "deleted"
            ]
          },
          "emailVerifiedAt": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time",
                "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
              },
              {
                "type": "null"
              }
            ]
          },
          "termsVersion": {
            "type": "string"
          },
          "termsAcceptedAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
          },
          "lastLoginAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
          },
          "locale": {
            "$ref": "#/components/schemas/UserLocale"
          }
        },
        "required": [
          "uid",
          "email",
          "name",
          "status",
          "emailVerifiedAt"
        ],
        "additionalProperties": false,
        "description": "The account record as stored, minus authentication method secrets."
      },
      "AccountDataExportWorkshopEntry": {
        "type": "object",
        "properties": {
          "workshop": {
            "$ref": "#/components/schemas/AccountDataExportWorkshop"
          },
          "grids": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AccountDataExportGrid"
            }
          },
          "estimates": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AccountDataExportEstimate"
            }
          },
          "orders": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AccountDataExportOrder"
            }
          }
        },
        "required": [
          "workshop",
          "grids",
          "estimates",
          "orders"
        ],
        "additionalProperties": false,
        "description": "One owned or joined workshop and everything in it."
      },
      "AccountDataExportWorkshop": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "tagline": {
            "type": "string"
          },
          "logo": {
            "type": "string"
          },
          "crafterName": {
            "type": "string"
          },
          "crafterAvatarSeed": {
            "type": "string"
          },
          "background": {
            "$ref": "#/components/schemas/Background"
          },
          "customBackground": {
            "$ref": "#/components/schemas/WorkshopCustomBackground"
          },
          "titleFont": {
            "$ref": "#/components/schemas/TitleFont"
          },
          "accentColor": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/HexColor"
              },
              {
                "type": "null"
              }
            ]
          },
          "plan": {
            "$ref": "#/components/schemas/WorkshopPlan"
          },
          "planExpiresAt": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time",
                "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
              },
              {
                "type": "null"
              }
            ]
          },
          "planSource": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/WorkshopPlanSource"
              },
              {
                "type": "null"
              }
            ]
          },
          "billingCustomerId": {
            "type": "string"
          },
          "billingSubscriptionId": {
            "type": [
              "string",
              "null"
            ]
          },
          "billingCancelAtPeriodEnd": {
            "type": "boolean"
          },
          "context": {
            "$ref": "#/components/schemas/WorkshopContext"
          },
          "contextDerived": {
            "$ref": "#/components/schemas/WorkshopContextDerived"
          },
          "contextSynthesisAttempts": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "date-time",
              "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
            }
          },
          "messageDraftAttempts": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "date-time",
              "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
            }
          },
          "isDefault": {
            "type": "boolean"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
          }
        },
        "required": [
          "id",
          "name",
          "tagline",
          "logo",
          "crafterName",
          "crafterAvatarSeed",
          "background",
          "titleFont",
          "isDefault",
          "createdAt"
        ],
        "additionalProperties": false,
        "description": "A workshop the account owns or belongs to, as stored: unlike the public Workshop resource this includes the raw billing ids and the AI rate-limit attempt timestamps, since this endpoint exports the record as kept, not the trimmed member-facing view."
      },
      "Background": {
        "type": "string",
        "enum": [
          "paper",
          "linen",
          "sage",
          "mist",
          "blush",
          "indigo",
          "forest",
          "plum",
          "terracotta",
          "charcoal",
          "astral",
          "ember",
          "meadow",
          "dusk",
          "tide",
          "orchid",
          "copper",
          "aurora",
          "dawn",
          "cloud"
        ],
        "description": "A preset background id for the workshop's poster. The id → CSS mapping is owned by the frontend."
      },
      "WorkshopCustomBackground": {
        "anyOf": [
          {
            "anyOf": [
              {
                "type": "object",
                "properties": {
                  "kind": {
                    "type": "string",
                    "const": "solid"
                  },
                  "from": {
                    "$ref": "#/components/schemas/HexColor"
                  }
                },
                "required": [
                  "kind",
                  "from"
                ],
                "additionalProperties": false
              },
              {
                "type": "object",
                "properties": {
                  "kind": {
                    "type": "string",
                    "const": "gradient"
                  },
                  "from": {
                    "$ref": "#/components/schemas/HexColor"
                  },
                  "to": {
                    "$ref": "#/components/schemas/HexColor"
                  }
                },
                "required": [
                  "kind",
                  "from",
                  "to"
                ],
                "additionalProperties": false
              }
            ]
          },
          {
            "type": "null"
          }
        ],
        "description": "A freeform solid or gradient background overriding the preset `background` id, or null for no override."
      },
      "HexColor": {
        "type": "string",
        "pattern": "^#[0-9A-Fa-f]{6}$",
        "description": "A #RRGGBB hex color."
      },
      "TitleFont": {
        "type": "string",
        "enum": [
          "sans",
          "serif",
          "didone",
          "mono",
          "rounded"
        ],
        "description": "A preset title font id for the workshop's poster. The id → font mapping is owned by the frontend."
      },
      "WorkshopPlan": {
        "type": "string",
        "enum": [
          "free",
          "pro"
        ],
        "description": "The workshop's resolved plan; an expired time-boxed Pro reads back as free."
      },
      "WorkshopPlanSource": {
        "type": "string",
        "enum": [
          "grant",
          "trial",
          "subscription"
        ],
        "description": "What granted the current Pro plan: a founder CLI grant, the mocked self-serve trial, or a paid subscription."
      },
      "WorkshopContext": {
        "type": "object",
        "properties": {
          "notes": {
            "type": "string"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
          }
        },
        "required": [
          "notes",
          "updatedAt"
        ],
        "additionalProperties": false,
        "description": "The workshop's context notes as stored: free text describing the workshop and the moment the server last saved them."
      },
      "WorkshopContextDerived": {
        "type": "object",
        "properties": {
          "summary": {
            "description": "At most 400 characters per language",
            "$ref": "#/components/schemas/LocalizedText"
          },
          "suggestedIntents": {
            "type": "object",
            "properties": {
              "en": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "pl": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            },
            "required": [
              "en",
              "pl"
            ],
            "additionalProperties": false,
            "description": "3 to 5 entries per language, at most 80 characters each"
          },
          "sourceUpdatedAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "description": "The `context.updatedAt` this was derived from"
          },
          "generatedAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
          }
        },
        "required": [
          "summary",
          "suggestedIntents",
          "sourceUpdatedAt",
          "generatedAt"
        ],
        "additionalProperties": false,
        "description": "The AI-synthesized summary and suggested intents distilled from the workshop's context notes."
      },
      "LocalizedText": {
        "type": "object",
        "properties": {
          "en": {
            "type": "string"
          },
          "pl": {
            "type": "string"
          }
        },
        "required": [
          "en",
          "pl"
        ],
        "additionalProperties": false,
        "description": "The same text in English and Polish."
      },
      "AccountDataExportGrid": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "workshopId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "iconSize": {
            "type": "string",
            "enum": [
              "small",
              "medium",
              "large"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
          },
          "buttons": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AccountDataExportGridButton"
            }
          }
        },
        "required": [
          "id",
          "workshopId",
          "name",
          "iconSize",
          "createdAt",
          "updatedAt",
          "buttons"
        ],
        "additionalProperties": false,
        "description": "A grid belonging to the workshop, as stored."
      },
      "AccountDataExportGridButton": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "emoji": {
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "unit": {
            "type": "string",
            "enum": [
              "sqm",
              "linear_m",
              "piece",
              "pair",
              "hour",
              "job",
              "freeform"
            ]
          },
          "unitPrice": {
            "type": "number"
          },
          "backgroundColor": {
            "type": "string"
          },
          "iconFileId": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "emoji",
          "label",
          "unit",
          "unitPrice"
        ],
        "additionalProperties": false,
        "description": "One button on a grid, as stored."
      },
      "AccountDataExportEstimate": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "workshopId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "recipientName": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
          },
          "markupPercent": {
            "type": "number"
          },
          "discountPercent": {
            "type": "number"
          },
          "lineItems": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AccountDataExportLineItem"
            }
          },
          "revisions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AccountDataExportEstimateRevision"
            }
          }
        },
        "required": [
          "id",
          "workshopId",
          "name",
          "createdAt",
          "updatedAt",
          "markupPercent",
          "discountPercent",
          "lineItems",
          "revisions"
        ],
        "additionalProperties": false,
        "description": "An estimate belonging to the workshop, with its saved revisions. Its PDF exports and hand-offs are not included (tracked in MR-44)."
      },
      "AccountDataExportLineItem": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "unit": {
            "type": "string",
            "enum": [
              "sqm",
              "linear_m",
              "piece",
              "pair",
              "hour",
              "job",
              "freeform"
            ]
          },
          "unitPrice": {
            "type": "number"
          },
          "quantity": {
            "type": "number"
          },
          "sourceButtonId": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "label",
          "unit",
          "unitPrice",
          "quantity"
        ],
        "additionalProperties": false,
        "description": "One line item on an estimate or one of its revisions, as stored."
      },
      "AccountDataExportEstimateRevision": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "revisionNumber": {
            "type": "number"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
          },
          "createdByUid": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "markupPercent": {
            "type": "number"
          },
          "discountPercent": {
            "type": "number"
          },
          "lineItems": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AccountDataExportLineItem"
            }
          }
        },
        "required": [
          "id",
          "revisionNumber",
          "createdAt",
          "createdByUid",
          "name",
          "markupPercent",
          "discountPercent",
          "lineItems"
        ],
        "additionalProperties": false,
        "description": "A saved revision of an estimate, as stored."
      },
      "AccountDataExportOrder": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "workshopId": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "customerName": {
            "type": "string"
          },
          "notes": {
            "type": "string"
          },
          "status": {
            "$ref": "#/components/schemas/OrderStatus"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
          },
          "lastActivityAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
          },
          "statusHistory": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OrderStatusHistoryEntry"
            }
          }
        },
        "required": [
          "id",
          "workshopId",
          "title",
          "customerName",
          "notes",
          "status",
          "createdAt",
          "lastActivityAt",
          "statusHistory"
        ],
        "additionalProperties": false,
        "description": "An order belonging to the workshop, as stored."
      },
      "OrderStatus": {
        "type": "string",
        "enum": [
          "todo",
          "in_progress",
          "done"
        ],
        "description": "An order's stage, always one of these three values in this order: todo, in_progress, done."
      },
      "OrderStatusHistoryEntry": {
        "type": "object",
        "properties": {
          "status": {
            "$ref": "#/components/schemas/OrderStatus"
          },
          "at": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
          }
        },
        "required": [
          "status",
          "at"
        ],
        "additionalProperties": false,
        "description": "One entry in an order's status history: the status it moved to and when."
      },
      "AccountDataExportFeedback": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "kind": {
            "$ref": "#/components/schemas/FeedbackKind"
          },
          "title": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "email": {
            "type": "string",
            "format": "email",
            "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
          },
          "locale": {
            "$ref": "#/components/schemas/UserLocale"
          },
          "path": {
            "type": "string"
          },
          "userAgent": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
          }
        },
        "required": [
          "id",
          "kind",
          "title",
          "message",
          "email",
          "locale",
          "createdAt"
        ],
        "additionalProperties": false,
        "description": "One feedback or support message the account submitted."
      },
      "FeedbackKind": {
        "type": "string",
        "enum": [
          "feedback",
          "support"
        ],
        "description": "Whether the message is general feedback or a support request."
      },
      "AccountDataExportAuthProvider": {
        "type": "object",
        "properties": {
          "provider": {
            "type": "string",
            "const": "google"
          },
          "subject": {
            "type": "string"
          },
          "linkedAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
          }
        },
        "required": [
          "provider",
          "subject",
          "linkedAt"
        ],
        "additionalProperties": false,
        "description": "A linked OAuth provider identity, as stored."
      },
      "WorkshopsList": {
        "type": "object",
        "properties": {
          "workshops": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Workshop"
            }
          }
        },
        "required": [
          "workshops"
        ],
        "additionalProperties": false
      },
      "Workshop": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "tagline": {
            "type": "string"
          },
          "logo": {
            "type": "string",
            "description": "One emoji from the curated icon set"
          },
          "crafterName": {
            "type": "string"
          },
          "crafterAvatarSeed": {
            "type": "string"
          },
          "background": {
            "$ref": "#/components/schemas/Background"
          },
          "customBackground": {
            "$ref": "#/components/schemas/WorkshopCustomBackground"
          },
          "titleFont": {
            "$ref": "#/components/schemas/TitleFont"
          },
          "accentColor": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/HexColor"
              },
              {
                "type": "null"
              }
            ]
          },
          "plan": {
            "$ref": "#/components/schemas/WorkshopPlan"
          },
          "planExpiresAt": {
            "description": "Set while `plan` is a time-boxed Pro (a trial or a paid subscription's current period); null on a permanent plan",
            "anyOf": [
              {
                "type": "string",
                "format": "date-time",
                "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
              },
              {
                "type": "null"
              }
            ]
          },
          "planSource": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/WorkshopPlanSource"
              },
              {
                "type": "null"
              }
            ]
          },
          "context": {
            "$ref": "#/components/schemas/WorkshopContext"
          },
          "contextDerived": {
            "$ref": "#/components/schemas/WorkshopContextDerived"
          },
          "isDefault": {
            "type": "boolean"
          },
          "members": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WorkshopMember"
            }
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
          }
        },
        "required": [
          "id",
          "name",
          "tagline",
          "logo",
          "crafterName",
          "crafterAvatarSeed",
          "background",
          "titleFont",
          "plan",
          "isDefault",
          "members",
          "createdAt"
        ],
        "additionalProperties": false,
        "description": "A workshop as returned to a member: billing ids and AI rate-limit attempt lists are stripped, the plan is already resolved for expiry."
      },
      "WorkshopMember": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The member's user id"
          },
          "name": {
            "type": "string"
          },
          "avatarSeed": {
            "type": "string"
          },
          "role": {
            "type": "string",
            "enum": [
              "owner",
              "member"
            ]
          }
        },
        "required": [
          "id",
          "name",
          "avatarSeed",
          "role"
        ],
        "additionalProperties": false,
        "description": "A workshop member, resolved from its membership record."
      },
      "Emoji": {
        "type": "string",
        "minLength": 1,
        "description": "One emoji from Mercastra's curated icon set (see EMOJI_CATEGORIES)."
      },
      "WorkshopEntitlementsUsage": {
        "type": "object",
        "properties": {
          "plan": {
            "$ref": "#/components/schemas/WorkshopPlan"
          },
          "planExpiresAt": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time",
                "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
              },
              {
                "type": "null"
              }
            ]
          },
          "brandMark": {
            "type": "object",
            "properties": {
              "canDisable": {
                "type": "boolean"
              }
            },
            "required": [
              "canDisable"
            ],
            "additionalProperties": false
          },
          "fileStorage": {
            "type": "object",
            "properties": {
              "usedBytes": {
                "type": "number"
              },
              "limitBytes": {
                "type": "number"
              }
            },
            "required": [
              "usedBytes",
              "limitBytes"
            ],
            "additionalProperties": false
          },
          "revisions": {
            "type": "object",
            "properties": {
              "limitPerEstimate": {
                "type": [
                  "number",
                  "null"
                ]
              }
            },
            "required": [
              "limitPerEstimate"
            ],
            "additionalProperties": false,
            "description": "null means unlimited (Pro); the value is per estimate, it carries no usage counter"
          },
          "billing": {
            "$ref": "#/components/schemas/WorkshopBilling"
          }
        },
        "required": [
          "plan",
          "planExpiresAt",
          "brandMark",
          "fileStorage",
          "revisions",
          "billing"
        ],
        "additionalProperties": false,
        "description": "The workshop's plan entitlements and current quota usage, derived from stored state only."
      },
      "WorkshopBilling": {
        "type": "object",
        "properties": {
          "hasAccount": {
            "type": "boolean",
            "description": "True once a payment-provider customer exists for the workshop"
          },
          "planSource": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/WorkshopPlanSource"
              },
              {
                "type": "null"
              }
            ]
          },
          "cancelAtPeriodEnd": {
            "type": "boolean",
            "description": "True only for a subscription the customer already canceled at the provider"
          },
          "checkoutAvailability": {
            "type": "string",
            "enum": [
              "available",
              "pro_purchase_disabled",
              "payments_not_configured"
            ]
          }
        },
        "required": [
          "hasAccount",
          "planSource",
          "cancelAtPeriodEnd",
          "checkoutAvailability"
        ],
        "additionalProperties": false
      },
      "OrdersList": {
        "type": "object",
        "properties": {
          "orders": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Order"
            }
          }
        },
        "required": [
          "orders"
        ],
        "additionalProperties": false
      },
      "Order": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "workshopId": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "customerName": {
            "type": "string",
            "description": "Blank when the order has no named customer"
          },
          "notes": {
            "type": "string"
          },
          "status": {
            "$ref": "#/components/schemas/OrderStatus"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
          },
          "lastActivityAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
          },
          "statusHistory": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OrderStatusHistoryEntry"
            }
          }
        },
        "required": [
          "id",
          "workshopId",
          "title",
          "customerName",
          "notes",
          "status",
          "createdAt",
          "lastActivityAt",
          "statusHistory"
        ],
        "additionalProperties": false,
        "description": "An order as returned to a member: title, customer name and notes are edited in place through this same shape, status changes are recorded in statusHistory."
      },
      "FileRecord": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "scope": {
            "$ref": "#/components/schemas/FileScope"
          },
          "scopeId": {
            "type": "string"
          },
          "category": {
            "$ref": "#/components/schemas/FileCategory"
          },
          "name": {
            "type": "string"
          },
          "contentType": {
            "type": "string"
          },
          "sizeBytes": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "uploadedByUid": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
          },
          "estimateId": {
            "description": "Set only for files created against an estimate scope.",
            "type": "string"
          }
        },
        "required": [
          "id",
          "scope",
          "scopeId",
          "category",
          "contentType",
          "sizeBytes",
          "uploadedByUid",
          "createdAt"
        ],
        "additionalProperties": false,
        "description": "A stored file's metadata. Omits the internal GCS storage path; download the bytes through GET /files/{fileId}."
      },
      "FileScope": {
        "type": "string",
        "enum": [
          "estimate",
          "workshop"
        ],
        "description": "The kind of resource a file is authorized against: workshop or estimate. Both categories accepted by POST /files resolve to workshop; estimate-scoped files are created by other endpoints, not this one."
      },
      "FileCategory": {
        "type": "string",
        "enum": [
          "grid_tile_icon",
          "estimate_pdf"
        ],
        "description": "What a file is used for. Decides the accepted content types and any per-file size cap: grid_tile_icon (an image attached to a grid button, up to 512 KB) or estimate_pdf (a generated estimate PDF, no per-file cap)."
      },
      "FilesList": {
        "type": "object",
        "properties": {
          "files": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FileRecord"
            }
          }
        },
        "required": [
          "files"
        ],
        "additionalProperties": false
      },
      "EstimatesList": {
        "type": "object",
        "properties": {
          "estimates": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Estimate"
            }
          }
        },
        "required": [
          "estimates"
        ],
        "additionalProperties": false
      },
      "Estimate": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "workshopId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "recipientName": {
            "description": "Absent when the estimate has no named recipient",
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
          },
          "markupPercent": {
            "type": "number"
          },
          "discountPercent": {
            "type": "number"
          },
          "lineItems": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LineItem"
            }
          }
        },
        "required": [
          "id",
          "workshopId",
          "name",
          "createdAt",
          "updatedAt",
          "markupPercent",
          "discountPercent",
          "lineItems"
        ],
        "additionalProperties": false,
        "description": "An estimate as returned to a member. Confirming it (see the confirm endpoint) snapshots this content into a new revision; nothing about the estimate itself records whether it has ever been confirmed."
      },
      "LineItem": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "unit": {
            "$ref": "#/components/schemas/LineItemUnit"
          },
          "unitPrice": {
            "type": "number"
          },
          "quantity": {
            "type": "number"
          },
          "sourceButtonId": {
            "description": "The grid button this line item was added from, when it was. Not validated on write.",
            "type": "string"
          }
        },
        "required": [
          "id",
          "label",
          "unit",
          "unitPrice",
          "quantity"
        ],
        "additionalProperties": false,
        "description": "One priced line on an estimate."
      },
      "LineItemUnit": {
        "type": "string",
        "enum": [
          "sqm",
          "linear_m",
          "piece",
          "pair",
          "hour",
          "job",
          "freeform"
        ],
        "description": "A line item's unit of measure, one of these seven values."
      },
      "MostUsedLineItemsList": {
        "type": "object",
        "properties": {
          "lineItems": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "label": {
                  "type": "string"
                },
                "unit": {
                  "$ref": "#/components/schemas/LineItemUnit"
                },
                "unitPrice": {
                  "type": "number"
                },
                "count": {
                  "type": "number",
                  "description": "How many of the workshop's estimates use a line item with this label and unit"
                },
                "lastUsedAt": {
                  "type": "string",
                  "format": "date-time",
                  "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
                  "description": "The updatedAt of the most recent estimate that uses it"
                }
              },
              "required": [
                "label",
                "unit",
                "unitPrice",
                "count",
                "lastUsedAt"
              ],
              "additionalProperties": false
            }
          }
        },
        "required": [
          "lineItems"
        ],
        "additionalProperties": false
      },
      "EstimateConfirmation": {
        "type": "object",
        "properties": {
          "estimate": {
            "$ref": "#/components/schemas/Estimate"
          },
          "revision": {
            "$ref": "#/components/schemas/EstimateRevision"
          }
        },
        "required": [
          "estimate",
          "revision"
        ],
        "additionalProperties": false
      },
      "EstimateRevision": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "revisionNumber": {
            "type": "number"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
          },
          "createdByUid": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "markupPercent": {
            "type": "number"
          },
          "discountPercent": {
            "type": "number"
          },
          "lineItems": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LineItem"
            }
          }
        },
        "required": [
          "id",
          "revisionNumber",
          "createdAt",
          "createdByUid",
          "name",
          "markupPercent",
          "discountPercent",
          "lineItems"
        ],
        "additionalProperties": false,
        "description": "A snapshot of an estimate's content at the moment it was confirmed, numbered from 1."
      },
      "EstimateRevisionsList": {
        "type": "object",
        "properties": {
          "revisions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EstimateRevision"
            }
          }
        },
        "required": [
          "revisions"
        ],
        "additionalProperties": false
      },
      "PdfExport": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "revisionNumber": {
            "type": "number",
            "description": "The estimate's latest revision number at the time of export."
          },
          "toggles": {
            "$ref": "#/components/schemas/PdfExportOptions"
          },
          "generatedAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
          },
          "createdByUid": {
            "type": "string"
          },
          "locale": {
            "description": "Absent only on a pdf export generated before locale was tracked.",
            "$ref": "#/components/schemas/DocumentLocale"
          },
          "preliminary": {
            "description": "Absent only on a pdf export generated before preliminary was tracked.",
            "type": "boolean"
          }
        },
        "required": [
          "id",
          "revisionNumber",
          "toggles",
          "generatedAt",
          "createdByUid"
        ],
        "additionalProperties": false,
        "description": "Metadata for one generated estimate PDF; this shape never carries the file's bytes, download them with GET /estimates/{estimateId}/pdf-exports/{pdfExportId}."
      },
      "PdfExportOptions": {
        "type": "object",
        "properties": {
          "breakdown": {
            "default": true,
            "description": "Include the itemized line-item breakdown table.",
            "type": "boolean"
          },
          "showMarkup": {
            "default": false,
            "description": "Show the markup percentage and amount.",
            "type": "boolean"
          },
          "showDiscount": {
            "default": false,
            "description": "Show the discount percentage and amount.",
            "type": "boolean"
          },
          "showRecipient": {
            "default": true,
            "description": "Show the recipient's name.",
            "type": "boolean"
          },
          "showBrandMark": {
            "default": true,
            "description": "Show Mercastra's brand mark. A workshop whose plan cannot disable it (see entitlements) has this forced back to true server-side regardless of what is sent.",
            "type": "boolean"
          }
        },
        "required": [
          "breakdown",
          "showMarkup",
          "showDiscount",
          "showRecipient",
          "showBrandMark"
        ],
        "additionalProperties": false,
        "description": "The visual toggles for a generated estimate PDF, each defaulting server-side when omitted."
      },
      "DocumentLocale": {
        "type": "string",
        "enum": [
          "pl",
          "en"
        ],
        "description": "The language a generated estimate PDF or hand-off message is written in: pl (Polish) or en (English)."
      },
      "PdfExportsList": {
        "type": "object",
        "properties": {
          "pdfExports": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PdfExport"
            }
          }
        },
        "required": [
          "pdfExports"
        ],
        "additionalProperties": false
      },
      "Handoff": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "kind": {
            "$ref": "#/components/schemas/HandoffKind"
          },
          "locale": {
            "$ref": "#/components/schemas/DocumentLocale"
          },
          "preliminary": {
            "type": "boolean"
          },
          "tone": {
            "$ref": "#/components/schemas/MessageTone"
          },
          "aiDrafted": {
            "type": "boolean",
            "description": "Whether the delivered message text came from POST .../message-draft rather than being hand-written."
          },
          "revisionNumber": {
            "type": "number",
            "description": "The estimate's latest revision number at the time of hand-off."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
          },
          "createdByUid": {
            "type": "string"
          },
          "pdfExportId": {
            "description": "The pdf export this hand-off refers to, when one was involved. Absent otherwise.",
            "type": "string"
          }
        },
        "required": [
          "id",
          "kind",
          "locale",
          "preliminary",
          "tone",
          "aiDrafted",
          "revisionNumber",
          "createdAt",
          "createdByUid"
        ],
        "additionalProperties": false,
        "description": "A record that an estimate was handed off to its recipient somehow - not the message or PDF itself, just that it happened, when, how and in what tone."
      },
      "HandoffKind": {
        "type": "string",
        "enum": [
          "message-copied",
          "message-shared",
          "pdf-shared",
          "pdf-and-message-shared",
          "email-opened"
        ],
        "description": "How a crafter delivered an estimate to its recipient, one of these five values: message-copied, message-shared, pdf-shared, pdf-and-message-shared or email-opened. Caller-supplied and not verified against anything that actually happened, see the hand-off endpoint's own description."
      },
      "MessageTone": {
        "type": "string",
        "enum": [
          "formal",
          "friendly"
        ],
        "description": "The tone of a hand-off message, formal or friendly. Recorded on every hand-off regardless of aiDrafted, not just ones whose text came from the AI draft endpoint."
      },
      "HandoffsList": {
        "type": "object",
        "properties": {
          "handoffs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Handoff"
            }
          }
        },
        "required": [
          "handoffs"
        ],
        "additionalProperties": false
      },
      "GridsList": {
        "type": "object",
        "properties": {
          "grids": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Grid"
            }
          }
        },
        "required": [
          "grids"
        ],
        "additionalProperties": false
      },
      "Grid": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "workshopId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "iconSize": {
            "$ref": "#/components/schemas/GridIconSize"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
          },
          "buttons": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GridButton"
            }
          }
        },
        "required": [
          "id",
          "workshopId",
          "name",
          "iconSize",
          "createdAt",
          "updatedAt",
          "buttons"
        ],
        "additionalProperties": false,
        "description": "A grid as returned to a member: a named board of priceable buttons, capped at 10 per workshop."
      },
      "GridIconSize": {
        "type": "string",
        "enum": [
          "small",
          "medium",
          "large"
        ],
        "description": "A grid's icon size, one of these three values."
      },
      "GridButton": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "emoji": {
            "$ref": "#/components/schemas/Emoji"
          },
          "label": {
            "type": "string"
          },
          "unit": {
            "$ref": "#/components/schemas/LineItemUnit"
          },
          "unitPrice": {
            "type": "number"
          },
          "backgroundColor": {
            "description": "Absent when the button has no background override",
            "$ref": "#/components/schemas/HexColor"
          },
          "iconFileId": {
            "description": "Id of a file uploaded through POST /files with category grid_tile_icon. Absent when the button has no icon.",
            "type": "string"
          }
        },
        "required": [
          "id",
          "emoji",
          "label",
          "unit",
          "unitPrice"
        ],
        "additionalProperties": false,
        "description": "One priceable button on a grid. unit and unitPrice become the sourceButtonId'd line item's defaults when added to an estimate."
      },
      "GridIconInUse": {
        "type": "object",
        "properties": {
          "gridIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "gridIds"
        ],
        "additionalProperties": false,
        "description": "The ids of every grid in the workshop with a button still referencing this icon."
      },
      "PredefinedItemsCatalog": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PredefinedGridItem"
            }
          },
          "domains": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PredefinedItemDomain"
            }
          },
          "templates": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GridTemplate"
            }
          }
        },
        "required": [
          "items",
          "domains",
          "templates"
        ],
        "additionalProperties": false
      },
      "PredefinedGridItem": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "emoji": {
            "$ref": "#/components/schemas/Emoji"
          },
          "label": {
            "$ref": "#/components/schemas/LocalizedText"
          },
          "unit": {
            "$ref": "#/components/schemas/LineItemUnit"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "id",
          "emoji",
          "label",
          "unit",
          "tags"
        ],
        "additionalProperties": false,
        "description": "One reference item a grid button can be built from: a label, a default unit and free-text tags."
      },
      "PredefinedItemDomain": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string"
          },
          "label": {
            "$ref": "#/components/schemas/LocalizedText"
          }
        },
        "required": [
          "slug",
          "label"
        ],
        "additionalProperties": false,
        "description": "One craft domain grouping catalog items and templates, for example woodworking-and-carpentry."
      },
      "GridTemplate": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string"
          },
          "domainSlug": {
            "type": "string",
            "description": "References a PredefinedItemDomain's slug, by string match, not enforced"
          },
          "name": {
            "$ref": "#/components/schemas/LocalizedText"
          },
          "description": {
            "$ref": "#/components/schemas/LocalizedText"
          },
          "itemIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "References PredefinedGridItem ids, by string match, not enforced"
          }
        },
        "required": [
          "slug",
          "domainSlug",
          "name",
          "description",
          "itemIds"
        ],
        "additionalProperties": false,
        "description": "A named starting set of catalog items for one domain, ready to turn into a grid's buttons."
      }
    },
    "securitySchemes": {
      "sessionCookie": {
        "type": "apiKey",
        "in": "cookie",
        "name": "session",
        "description": "The httpOnly session cookie the browser attaches automatically after signing in. Mutating requests (anything but GET, HEAD and OPTIONS) must also send the X-CSRF-Token header echoing the value of the csrf-token cookie, or they are refused with 403."
      }
    }
  }
}