{ "openapi": "3.0.3", "info": { "title": "Dash API", "version": "1.0.0", "description": "Backend API for Dash — Google SSO auth, MSSQL, session cookies.\n\nBrowser clients must send cookies (`credentials: 'include'`). Sessions use an HTTP-only `token` cookie (`Secure`, `SameSite=Strict`, 24h); HTTPS is required for login." }, "servers": [ { "url": "http://localhost:4208/api", "description": "Local default (PORT / .env)" } ], "tags": [ { "name": "Auth", "description": "Google SSO and session" }, { "name": "Health", "description": "Connectivity checks" }, { "name": "Events", "description": "SSE update signals for live clients" }, { "name": "Article", "description": "Articles under categories" }, { "name": "Category", "description": "Category tree and membership" }, { "name": "Company", "description": "Company CRUD" }, { "name": "Unit", "description": "Company-scoped units of measure" }, { "name": "Brand", "description": "Company-scoped brands" }, { "name": "Supplier", "description": "Company-scoped suppliers (Lieferanten)" }, { "name": "Delivery", "description": "Company-scoped goods receipts (Lieferungen)" }, { "name": "Quote", "description": "Company-scoped quotes (Angebote)" }, { "name": "Order", "description": "Company-scoped sales orders (Aufträge)" }, { "name": "Customergroup", "description": "Company-scoped customer groups" }, { "name": "Entity", "description": "Company-scoped entities, addresses, and contacts" }, { "name": "Image", "description": "Content-addressed AVIF by fullHash" }, { "name": "Posterminal", "description": "Company-scoped POS terminals" } ], "paths": { "/unit": { "get": { "tags": [ "Unit" ], "summary": "List units", "description": "Lists units for the authenticated user's selected company (default-language symbol/name).", "security": [ { "cookieAuth": [] } ], "x-markdown": "unit/list.md", "responses": { "200": { "description": "Unit list", "content": { "application/json": { "schema": { "type": "object", "properties": { "units": { "type": "array", "items": { "$ref": "#/components/schemas/Unit" } } } } } } }, "400": { "description": "No company selected", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Company not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "put": { "tags": [ "Unit" ], "summary": "Upsert unit", "description": "Insert or update unit by `si` for the selected company. Upserts `unittext` for the company language.\n`basesifactor` must be finite and `> 0`.\n", "security": [ { "cookieAuth": [] } ], "x-markdown": "unit/upsert.md", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "si", "basesi", "basesifactor", "symbol", "name" ], "properties": { "si": { "type": "string" }, "basesi": { "type": "string" }, "basesifactor": { "type": "number" }, "symbol": { "type": "string" }, "name": { "type": "string" } }, "example": { "si": "l", "basesi": "l", "basesifactor": 1, "symbol": "L", "name": "Liter" } } } } }, "responses": { "200": { "description": "Upserted unit", "content": { "application/json": { "schema": { "type": "object", "properties": { "unit": { "$ref": "#/components/schemas/Unit" } } } } } }, "400": { "description": "Invalid unit or no company selected", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Company not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "delete": { "tags": [ "Unit" ], "summary": "Delete unit", "description": "Deletes `si` for the selected company. Fails with `Unit in use` if articles still reference it.", "security": [ { "cookieAuth": [] } ], "x-markdown": "unit/delete.md", "parameters": [ { "in": "query", "name": "si", "required": true, "schema": { "type": "string" } } ], "responses": { "204": { "description": "Deleted" }, "400": { "description": "Missing si or no company selected", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Company or unit not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "Unit in use", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/supplier": { "get": { "tags": [ "Supplier" ], "summary": "List suppliers", "description": "Lists suppliers (`entity` rows in the company `Lieferanten` customergroup)\nfor the authenticated user's selected company.\n", "security": [ { "cookieAuth": [] } ], "x-markdown": "supplier/list.md", "responses": { "200": { "description": "Supplier list", "content": { "application/json": { "schema": { "type": "object", "properties": { "suppliers": { "type": "array", "items": { "$ref": "#/components/schemas/Supplier" } } } } } } }, "400": { "description": "No company selected", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Company not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "post": { "tags": [ "Supplier" ], "summary": "Create supplier", "description": "Creates a supplier in the company `Lieferanten` group (created on first use).\n", "security": [ { "cookieAuth": [] } ], "x-markdown": "supplier/create.md", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "name" ], "properties": { "name": { "type": "string" } }, "example": { "name": "Großhandel Müller" } } } } }, "responses": { "201": { "description": "Created supplier", "content": { "application/json": { "schema": { "type": "object", "properties": { "supplier": { "$ref": "#/components/schemas/Supplier" } } } } } }, "400": { "description": "Name required or no company selected", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Company not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/supplier/{id}": { "patch": { "tags": [ "Supplier" ], "summary": "Update supplier", "description": "Renames a supplier for the selected company.", "security": [ { "cookieAuth": [] } ], "x-markdown": "supplier/update.md", "parameters": [ { "in": "path", "name": "id", "required": true, "schema": { "type": "integer" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "name" ], "properties": { "name": { "type": "string" } }, "example": { "name": "Großhandel Müller GmbH" } } } } }, "responses": { "200": { "description": "Updated supplier", "content": { "application/json": { "schema": { "type": "object", "properties": { "supplier": { "$ref": "#/components/schemas/Supplier" } } } } } }, "400": { "description": "Name required, invalid id, or no company selected", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Supplier or company not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "delete": { "tags": [ "Supplier" ], "summary": "Delete supplier", "description": "Deletes the supplier entity for the selected company.\nRelated `entityhistory` cascades; `manifest.entity_id` is set null.\n", "security": [ { "cookieAuth": [] } ], "x-markdown": "supplier/delete.md", "parameters": [ { "in": "path", "name": "id", "required": true, "schema": { "type": "integer" } } ], "responses": { "204": { "description": "Deleted" }, "400": { "description": "No company selected or invalid id", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Supplier not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/quote": { "get": { "tags": [ "Quote" ], "summary": "List quotes", "description": "Lists quotes (`manifest` rows with `type = 'quote'`)\nfor the authenticated user's selected company.\n", "security": [ { "cookieAuth": [] } ], "x-markdown": "quote/list.md", "responses": { "200": { "description": "Quote list", "content": { "application/json": { "schema": { "type": "object", "properties": { "quotes": { "type": "array", "items": { "$ref": "#/components/schemas/QuoteSummary" } } } } } } }, "400": { "description": "No company selected", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Company not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "post": { "tags": [ "Quote" ], "summary": "Create quote", "description": "Creates a quote with header and lines in one transaction.\n`date` is the document date (valid-until).\n`entityId` must be a company customer (not Lieferanten/POS), or null.\nEach line requires `sku`, `amount`, `net`, and `tax` (rate); `rebate` defaults to `0`.\n", "security": [ { "cookieAuth": [] } ], "x-markdown": "quote/create.md", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "number", "date", "lines" ], "properties": { "number": { "type": "string" }, "date": { "type": "string", "format": "date-time", "description": "Document date / valid-until" }, "entityId": { "type": "integer", "nullable": true, "description": "Customer entity (manifest.entity_id)" }, "lines": { "type": "array", "items": { "$ref": "#/components/schemas/ManifestLineInput" } } }, "example": { "number": "Q-2026-001", "date": "2026-08-31", "entityId": 1, "lines": [ { "sku": "1", "amount": 10, "net": 99.5, "tax": 19 } ] } } } } }, "responses": { "201": { "description": "Created quote", "content": { "application/json": { "schema": { "type": "object", "properties": { "quote": { "$ref": "#/components/schemas/Quote" } } } } } }, "400": { "description": "Invalid quote, invalid lines, or no company selected", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Company, entity, or article not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/quote/{id}": { "get": { "tags": [ "Quote" ], "summary": "Get quote", "description": "Returns a quote with lines for the selected company.", "security": [ { "cookieAuth": [] } ], "x-markdown": "quote/get.md", "parameters": [ { "in": "path", "name": "id", "required": true, "schema": { "type": "integer" } } ], "responses": { "200": { "description": "Quote", "content": { "application/json": { "schema": { "type": "object", "properties": { "quote": { "$ref": "#/components/schemas/Quote" } } } } } }, "400": { "description": "No company selected or invalid id", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Quote or company not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "patch": { "tags": [ "Quote" ], "summary": "Update quote", "description": "Patches header fields that are present. If `lines` is present, replaces\nall lines; if omitted, leaves lines unchanged. Returns the full quote.\n`date` is the document date (valid-until).\n", "security": [ { "cookieAuth": [] } ], "x-markdown": "quote/update.md", "parameters": [ { "in": "path", "name": "id", "required": true, "schema": { "type": "integer" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "number": { "type": "string" }, "date": { "type": "string", "format": "date-time", "description": "Document date / valid-until" }, "entityId": { "type": "integer", "nullable": true }, "lines": { "type": "array", "items": { "$ref": "#/components/schemas/ManifestLineInput" } } }, "example": { "number": "Q-2026-001b", "lines": [] } } } } }, "responses": { "200": { "description": "Updated quote", "content": { "application/json": { "schema": { "type": "object", "properties": { "quote": { "$ref": "#/components/schemas/Quote" } } } } } }, "400": { "description": "Invalid quote, invalid lines, invalid id, or no company selected", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Quote, entity, article, or company not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "delete": { "tags": [ "Quote" ], "summary": "Delete quote", "description": "Deletes the quote (`manifestarticle` rows cascade).", "security": [ { "cookieAuth": [] } ], "x-markdown": "quote/delete.md", "parameters": [ { "in": "path", "name": "id", "required": true, "schema": { "type": "integer" } } ], "responses": { "204": { "description": "Deleted" }, "400": { "description": "No company selected or invalid id", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Quote not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/posterminal": { "get": { "tags": [ "Posterminal" ], "summary": "List POS terminals", "description": "Lists POS terminals for the authenticated user's selected company.", "security": [ { "cookieAuth": [] } ], "x-markdown": "posterminal/list.md", "responses": { "200": { "description": "Posterminal list", "content": { "application/json": { "schema": { "type": "object", "properties": { "posterminals": { "type": "array", "items": { "$ref": "#/components/schemas/Posterminal" } } } } } } }, "400": { "description": "No company selected", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Company not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "put": { "tags": [ "Posterminal" ], "summary": "Create POS terminal", "description": "Creates a new POS terminal for the selected company.\nThe pairingpin is randomly generated server-side and returned in the response.\n", "security": [ { "cookieAuth": [] } ], "x-markdown": "posterminal/upsert.md", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "name" ], "properties": { "name": { "type": "string" } }, "example": { "name": "Kassa 1" } } } } }, "responses": { "200": { "description": "Created posterminal with generated pairingpin", "content": { "application/json": { "schema": { "type": "object", "properties": { "posterminal": { "$ref": "#/components/schemas/Posterminal" } } } } } }, "400": { "description": "Invalid posterminal or no company selected", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Company not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/posterminal/{id}": { "delete": { "tags": [ "Posterminal" ], "summary": "Delete POS terminal", "description": "Deletes a POS terminal for the selected company.", "security": [ { "cookieAuth": [] } ], "x-markdown": "posterminal/delete.md", "parameters": [ { "in": "path", "name": "id", "required": true, "schema": { "type": "integer" } } ], "responses": { "204": { "description": "Deleted" }, "400": { "description": "No company selected", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Posterminal not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/posterminal/{id}/category/{categoryId}": { "put": { "tags": [ "Posterminal" ], "summary": "Enable category on terminal", "description": "Toggles a single category's visibility on a terminal (idempotent).", "security": [ { "cookieAuth": [] } ], "x-markdown": "posterminal/setOn.md", "parameters": [ { "in": "path", "name": "id", "required": true, "schema": { "type": "integer" } }, { "in": "path", "name": "categoryId", "required": true, "schema": { "type": "integer" } } ], "responses": { "200": { "description": "Updated category IDs for this terminal", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "categoryIds": { "type": "array", "items": { "type": "integer" } } } } } } }, "400": { "description": "No company selected or invalid terminal/category", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Posterminal not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "delete": { "tags": [ "Posterminal" ], "summary": "Disable category on terminal", "description": "Removes a single category's visibility on a terminal (idempotent).", "security": [ { "cookieAuth": [] } ], "x-markdown": "posterminal/setOff.md", "parameters": [ { "in": "path", "name": "id", "required": true, "schema": { "type": "integer" } }, { "in": "path", "name": "categoryId", "required": true, "schema": { "type": "integer" } } ], "responses": { "200": { "description": "Updated category IDs for this terminal", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "categoryIds": { "type": "array", "items": { "type": "integer" } } } } } } }, "400": { "description": "No company selected or invalid terminal/category", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Posterminal not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/posterminal/{id}/all": { "put": { "tags": [ "Posterminal" ], "summary": "Enable all categories on terminal", "description": "Walks the company's category tree and inserts any missing links\nfor this terminal. Returns the updated category IDs.\n", "security": [ { "cookieAuth": [] } ], "x-markdown": "posterminal/setOnAll.md", "parameters": [ { "in": "path", "name": "id", "required": true, "schema": { "type": "integer" } } ], "responses": { "200": { "description": "Updated category IDs for this terminal", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "categoryIds": { "type": "array", "items": { "type": "integer" } } } } } } }, "400": { "description": "No company selected or invalid terminal", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Posterminal not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "delete": { "tags": [ "Posterminal" ], "summary": "Disable all categories on terminal", "description": "Removes all category links for this terminal.", "security": [ { "cookieAuth": [] } ], "x-markdown": "posterminal/setOffAll.md", "parameters": [ { "in": "path", "name": "id", "required": true, "schema": { "type": "integer" } } ], "responses": { "200": { "description": "Updated category IDs for this terminal", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "categoryIds": { "type": "array", "items": { "type": "integer" } } } } } } }, "400": { "description": "No company selected or invalid terminal", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Posterminal not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/image/{fullHash}": { "get": { "tags": [ "Image" ], "summary": "Full image by hash", "description": "Returns the full AVIF by `pic.hash` (SHA-256 hex).\nAuth via article category access. Browser-cacheable (`private`, `immutable`, `ETag`).\n", "security": [ { "cookieAuth": [] } ], "x-markdown": "image/get.md", "parameters": [ { "in": "path", "name": "fullHash", "required": true, "schema": { "type": "string", "pattern": "^[a-fA-F0-9]{64}$" }, "description": "SHA-256 hex of the full image bytes" } ], "responses": { "200": { "description": "AVIF image bytes", "headers": { "Cache-Control": { "schema": { "type": "string", "example": "private, max-age=31536000, immutable" } }, "ETag": { "schema": { "type": "string" } } }, "content": { "image/avif": { "schema": { "type": "string", "format": "binary" } } } }, "304": { "description": "Not modified (If-None-Match matched ETag)" }, "400": { "description": "Invalid hash", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Image not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/thumb/{fullHash}": { "get": { "tags": [ "Image" ], "summary": "Thumb image by hash", "description": "Returns the thumb AVIF for `pic.hash` (fullHash — not thumbhash).\nAuth via article category access. Browser-cacheable (`private`, `immutable`, `ETag`).\n", "security": [ { "cookieAuth": [] } ], "x-markdown": "thumb/get.md", "parameters": [ { "in": "path", "name": "fullHash", "required": true, "schema": { "type": "string", "pattern": "^[a-fA-F0-9]{64}$" }, "description": "SHA-256 hex of the full image bytes" } ], "responses": { "200": { "description": "AVIF thumb bytes", "headers": { "Cache-Control": { "schema": { "type": "string", "example": "private, max-age=31536000, immutable" } }, "ETag": { "schema": { "type": "string" } } }, "content": { "image/avif": { "schema": { "type": "string", "format": "binary" } } } }, "304": { "description": "Not modified (If-None-Match matched ETag)" }, "400": { "description": "Invalid hash", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Image not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/order": { "get": { "tags": [ "Order" ], "summary": "List orders", "description": "Lists sales orders (`manifest` rows with `type = 'order'`)\nfor the authenticated user's selected company.\n", "security": [ { "cookieAuth": [] } ], "x-markdown": "order/list.md", "responses": { "200": { "description": "Order list", "content": { "application/json": { "schema": { "type": "object", "properties": { "orders": { "type": "array", "items": { "$ref": "#/components/schemas/OrderSummary" } } } } } } }, "400": { "description": "No company selected", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Company not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "post": { "tags": [ "Order" ], "summary": "Create order", "description": "Creates a sales order with header and lines in one transaction.\n`date` is the document date (expected delivery).\n`entityId` must be a company customer (not Lieferanten/POS), or null.\nEach line requires `sku`, `amount`, `net`, and `tax` (rate); `rebate` defaults to `0`.\n", "security": [ { "cookieAuth": [] } ], "x-markdown": "order/create.md", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "number", "date", "lines" ], "properties": { "number": { "type": "string" }, "date": { "type": "string", "format": "date-time", "description": "Document date / expected delivery" }, "entityId": { "type": "integer", "nullable": true, "description": "Customer entity (manifest.entity_id)" }, "lines": { "type": "array", "items": { "$ref": "#/components/schemas/ManifestLineInput" } } }, "example": { "number": "O-2026-001", "date": "2026-09-15", "entityId": 1, "lines": [ { "sku": "1", "amount": 10, "net": 99.5, "tax": 19 } ] } } } } }, "responses": { "201": { "description": "Created order", "content": { "application/json": { "schema": { "type": "object", "properties": { "order": { "$ref": "#/components/schemas/Order" } } } } } }, "400": { "description": "Invalid order, invalid lines, or no company selected", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Company, entity, or article not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/order/{id}": { "get": { "tags": [ "Order" ], "summary": "Get order", "description": "Returns a sales order with lines for the selected company.", "security": [ { "cookieAuth": [] } ], "x-markdown": "order/get.md", "parameters": [ { "in": "path", "name": "id", "required": true, "schema": { "type": "integer" } } ], "responses": { "200": { "description": "Order", "content": { "application/json": { "schema": { "type": "object", "properties": { "order": { "$ref": "#/components/schemas/Order" } } } } } }, "400": { "description": "No company selected or invalid id", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Order or company not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "patch": { "tags": [ "Order" ], "summary": "Update order", "description": "Patches header fields that are present. If `lines` is present, replaces\nall lines; if omitted, leaves lines unchanged. Returns the full order.\n`date` is the document date (expected delivery).\n", "security": [ { "cookieAuth": [] } ], "x-markdown": "order/update.md", "parameters": [ { "in": "path", "name": "id", "required": true, "schema": { "type": "integer" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "number": { "type": "string" }, "date": { "type": "string", "format": "date-time", "description": "Document date / expected delivery" }, "entityId": { "type": "integer", "nullable": true }, "lines": { "type": "array", "items": { "$ref": "#/components/schemas/ManifestLineInput" } } }, "example": { "number": "O-2026-001b", "lines": [] } } } } }, "responses": { "200": { "description": "Updated order", "content": { "application/json": { "schema": { "type": "object", "properties": { "order": { "$ref": "#/components/schemas/Order" } } } } } }, "400": { "description": "Invalid order, invalid lines, invalid id, or no company selected", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Order, entity, article, or company not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "delete": { "tags": [ "Order" ], "summary": "Delete order", "description": "Deletes the sales order (`manifestarticle` rows cascade).", "security": [ { "cookieAuth": [] } ], "x-markdown": "order/delete.md", "parameters": [ { "in": "path", "name": "id", "required": true, "schema": { "type": "integer" } } ], "responses": { "204": { "description": "Deleted" }, "400": { "description": "No company selected or invalid id", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Order not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/health": { "get": { "tags": [ "Health" ], "summary": "Database health check", "description": "Verifies the database client can establish a connection.", "security": [], "x-markdown": "health.md", "responses": { "200": { "description": "Database reachable", "content": { "application/json": { "schema": { "type": "object", "properties": { "ok": { "type": "boolean", "example": true } } } } } }, "503": { "description": "Database unreachable", "content": { "application/json": { "schema": { "type": "object", "properties": { "ok": { "type": "boolean", "example": false }, "error": { "type": "string" } } } } } } } } }, "/events": { "get": { "tags": [ "Events" ], "summary": "Subscribe to update signals (SSE)", "description": "Opens a Server-Sent Events stream for the authenticated user's\n**selected company**. Clients receive named signals when another\nrequest successfully mutates shared data for that company\n(including the same browser's other tabs).\n\nUse `EventSource` with credentials so the session cookie is sent:\n\n```js\nconst es = new EventSource('https://…/api/events', { withCredentials: true });\nes.addEventListener('category', () => { refetchCategoryTree(); });\nes.addEventListener('articles', () => { refetchArticleIndex(); });\nes.addEventListener('article-42000045', () => { refetchArticle('42000045'); });\n```\n\nKeep-alive comments (`: keepalive`) are sent roughly every 25s.\nReconnect after changing the selected company (the stream is bound\nto the company at connect time).\n\n## Signals\n\n| Signal | Emitted after successful |\n|--------|--------------------------|\n| `category` | `POST /category`, `PATCH /category/{id}`, `PUT /category/order`, `DELETE /category/{id}`, `POST /category/{id}/image`, `DELETE /category/{id}/image` |\n| `articles` | `POST /article`, `PATCH /article/{sku}`, `DELETE /article/{sku}` |\n| `article-` | `PUT /article/{sku}/weight`, `PUT /article/{sku}/prices`, `PUT /article/{sku}/eans`, `PUT /article/{sku}/categories`, `PUT /article/{sku}/brand`, `PUT /article/{sku}/images/order`, `POST /article/{sku}/image`, `DELETE /article/{sku}/image/{fullHash}` |\n\nSignals are only sent when the mutation actually succeeds (2xx).\nFailed or no-op validation errors do not emit. The `article-`\nevent name uses the concrete SKU (e.g. `article-42000045`).\n", "security": [ { "cookieAuth": [] } ], "x-markdown": "events.md", "responses": { "200": { "description": "SSE stream (`text/event-stream`). Each signal is an SSE event\nwhose name is the signal id and whose data is `{}`.\n", "content": { "text/event-stream": { "schema": { "type": "string", "example": "event: category\ndata: {}\n\nevent: articles\ndata: {}\n\nevent: article-42000045\ndata: {}\n" } } } }, "400": { "description": "No company selected", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/entity/{entityId}/contact": { "get": { "tags": [ "Entity" ], "summary": "List entity contacts", "description": "Lists contacts for an entity of the selected company (`contactType` e.g. `email`, `phone`).", "security": [ { "cookieAuth": [] } ], "x-markdown": "entity/contact/list.md", "parameters": [ { "in": "path", "name": "entityId", "required": true, "schema": { "type": "integer" } } ], "responses": { "200": { "description": "Contact list", "content": { "application/json": { "schema": { "type": "object", "properties": { "contacts": { "type": "array", "items": { "$ref": "#/components/schemas/EntityContact" } } } } } } }, "400": { "description": "No company selected or invalid entityId", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Entity or company not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "post": { "tags": [ "Entity" ], "summary": "Create entity contact", "description": "Creates a contact for the entity. `contactType` and `contact` are required.", "security": [ { "cookieAuth": [] } ], "x-markdown": "entity/contact/create.md", "parameters": [ { "in": "path", "name": "entityId", "required": true, "schema": { "type": "integer" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "contactType", "contact" ], "properties": { "contactType": { "type": "string", "description": "e.g. email, phone" }, "contact": { "type": "string" } }, "example": { "contactType": "email", "contact": "anna@example.com" } } } } }, "responses": { "201": { "description": "Created contact", "content": { "application/json": { "schema": { "type": "object", "properties": { "contact": { "$ref": "#/components/schemas/EntityContact" } } } } } }, "400": { "description": "Contact required, invalid entityId, or no company selected", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Entity or company not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/entity/{entityId}/contact/{id}": { "patch": { "tags": [ "Entity" ], "summary": "Update entity contact", "description": "Patches `contactType` and/or `contact`.", "security": [ { "cookieAuth": [] } ], "x-markdown": "entity/contact/update.md", "parameters": [ { "in": "path", "name": "entityId", "required": true, "schema": { "type": "integer" } }, { "in": "path", "name": "id", "required": true, "schema": { "type": "integer" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "contactType": { "type": "string" }, "contact": { "type": "string" } }, "example": { "contact": "anna.mueller@example.com" } } } } }, "responses": { "200": { "description": "Updated contact", "content": { "application/json": { "schema": { "type": "object", "properties": { "contact": { "$ref": "#/components/schemas/EntityContact" } } } } } }, "400": { "description": "Contact required, invalid ids, or no company selected", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Contact, entity, or company not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "delete": { "tags": [ "Entity" ], "summary": "Delete entity contact", "description": "Deletes one contact of the entity.", "security": [ { "cookieAuth": [] } ], "x-markdown": "entity/contact/delete.md", "parameters": [ { "in": "path", "name": "entityId", "required": true, "schema": { "type": "integer" } }, { "in": "path", "name": "id", "required": true, "schema": { "type": "integer" } } ], "responses": { "204": { "description": "Deleted" }, "400": { "description": "No company selected or invalid ids", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Contact or entity not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/entity/{entityId}/address": { "get": { "tags": [ "Entity" ], "summary": "List entity addresses", "description": "Lists addresses for an entity of the selected company.", "security": [ { "cookieAuth": [] } ], "x-markdown": "entity/address/list.md", "parameters": [ { "in": "path", "name": "entityId", "required": true, "schema": { "type": "integer" } } ], "responses": { "200": { "description": "Address list", "content": { "application/json": { "schema": { "type": "object", "properties": { "addresses": { "type": "array", "items": { "$ref": "#/components/schemas/EntityAddress" } } } } } } }, "400": { "description": "No company selected or invalid entityId", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Entity or company not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "post": { "tags": [ "Entity" ], "summary": "Create entity address", "description": "Creates an address for the entity. `addressType` is required.", "security": [ { "cookieAuth": [] } ], "x-markdown": "entity/address/create.md", "parameters": [ { "in": "path", "name": "entityId", "required": true, "schema": { "type": "integer" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "addressType" ], "properties": { "addressType": { "type": "string" }, "firstName": { "type": "string", "nullable": true }, "name": { "type": "string", "nullable": true }, "street": { "type": "string", "nullable": true }, "streetNumber": { "type": "string", "nullable": true }, "zip": { "type": "string", "nullable": true }, "city": { "type": "string", "nullable": true }, "county": { "type": "string", "nullable": true } }, "example": { "addressType": "billing", "street": "Hauptstraße", "streetNumber": "12", "zip": "10115", "city": "Berlin" } } } } }, "responses": { "201": { "description": "Created address", "content": { "application/json": { "schema": { "type": "object", "properties": { "address": { "$ref": "#/components/schemas/EntityAddress" } } } } } }, "400": { "description": "Address type required, invalid entityId, or no company selected", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Entity or company not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/entity/{entityId}/address/{id}": { "patch": { "tags": [ "Entity" ], "summary": "Update entity address", "description": "Patches address fields that are present.", "security": [ { "cookieAuth": [] } ], "x-markdown": "entity/address/update.md", "parameters": [ { "in": "path", "name": "entityId", "required": true, "schema": { "type": "integer" } }, { "in": "path", "name": "id", "required": true, "schema": { "type": "integer" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "addressType": { "type": "string" }, "firstName": { "type": "string", "nullable": true }, "name": { "type": "string", "nullable": true }, "street": { "type": "string", "nullable": true }, "streetNumber": { "type": "string", "nullable": true }, "zip": { "type": "string", "nullable": true }, "city": { "type": "string", "nullable": true }, "county": { "type": "string", "nullable": true } }, "example": { "city": "München", "zip": "80331" } } } } }, "responses": { "200": { "description": "Updated address", "content": { "application/json": { "schema": { "type": "object", "properties": { "address": { "$ref": "#/components/schemas/EntityAddress" } } } } } }, "400": { "description": "Address type required, invalid ids, or no company selected", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Address, entity, or company not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "delete": { "tags": [ "Entity" ], "summary": "Delete entity address", "description": "Deletes one address of the entity.", "security": [ { "cookieAuth": [] } ], "x-markdown": "entity/address/delete.md", "parameters": [ { "in": "path", "name": "entityId", "required": true, "schema": { "type": "integer" } }, { "in": "path", "name": "id", "required": true, "schema": { "type": "integer" } } ], "responses": { "204": { "description": "Deleted" }, "400": { "description": "No company selected or invalid ids", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Address or entity not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/entity": { "get": { "tags": [ "Entity" ], "summary": "List entities", "description": "Lists entities for the selected company. Optional `customergroupId`\nquery filters to one group (must belong to the company).\n", "security": [ { "cookieAuth": [] } ], "x-markdown": "entity/list.md", "parameters": [ { "in": "query", "name": "customergroupId", "required": false, "schema": { "type": "integer" } } ], "responses": { "200": { "description": "Entity list", "content": { "application/json": { "schema": { "type": "object", "properties": { "entities": { "type": "array", "items": { "$ref": "#/components/schemas/Entity" } } } } } } }, "400": { "description": "No company selected or invalid customergroupId", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Company or customer group not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "post": { "tags": [ "Entity" ], "summary": "Create entity", "description": "Creates an entity in a company customer group and writes `entityhistory` (`Erstellt`).\n", "security": [ { "cookieAuth": [] } ], "x-markdown": "entity/create.md", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "customergroupId", "name" ], "properties": { "customergroupId": { "type": "integer" }, "name": { "type": "string" }, "firstName": { "type": "string", "nullable": true } }, "example": { "customergroupId": 1, "name": "Müller", "firstName": "Anna" } } } } }, "responses": { "201": { "description": "Created entity", "content": { "application/json": { "schema": { "type": "object", "properties": { "entity": { "$ref": "#/components/schemas/Entity" } } } } } }, "400": { "description": "Name required or no company selected", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Company or customer group not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/entity/{id}": { "get": { "tags": [ "Entity" ], "summary": "Get entity", "description": "Returns one entity for the selected company.", "security": [ { "cookieAuth": [] } ], "x-markdown": "entity/get.md", "parameters": [ { "in": "path", "name": "id", "required": true, "schema": { "type": "integer" } } ], "responses": { "200": { "description": "Entity", "content": { "application/json": { "schema": { "type": "object", "properties": { "entity": { "$ref": "#/components/schemas/Entity" } } } } } }, "400": { "description": "No company selected or invalid id", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Entity or company not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "patch": { "tags": [ "Entity" ], "summary": "Update entity", "description": "Patches `name` and/or `firstName`. Writes `entityhistory` when values change.\n", "security": [ { "cookieAuth": [] } ], "x-markdown": "entity/update.md", "parameters": [ { "in": "path", "name": "id", "required": true, "schema": { "type": "integer" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string" }, "firstName": { "type": "string", "nullable": true } }, "example": { "name": "Müller", "firstName": "Anna" } } } } }, "responses": { "200": { "description": "Updated entity", "content": { "application/json": { "schema": { "type": "object", "properties": { "entity": { "$ref": "#/components/schemas/Entity" } } } } } }, "400": { "description": "Name required, invalid id, or no company selected", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Entity or company not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "delete": { "tags": [ "Entity" ], "summary": "Delete entity", "description": "Deletes the entity (`entityhistory` cascades; related addresses/contacts cascade).", "security": [ { "cookieAuth": [] } ], "x-markdown": "entity/delete.md", "parameters": [ { "in": "path", "name": "id", "required": true, "schema": { "type": "integer" } } ], "responses": { "204": { "description": "Deleted" }, "400": { "description": "No company selected or invalid id", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Entity not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/delivery": { "get": { "tags": [ "Delivery" ], "summary": "List deliveries", "description": "Lists goods receipts (`manifest` rows with `type = 'delivery'`)\nfor the authenticated user's selected company.\n", "security": [ { "cookieAuth": [] } ], "x-markdown": "delivery/list.md", "responses": { "200": { "description": "Delivery list", "content": { "application/json": { "schema": { "type": "object", "properties": { "deliveries": { "type": "array", "items": { "$ref": "#/components/schemas/DeliverySummary" } } } } } } }, "400": { "description": "No company selected", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Company not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "post": { "tags": [ "Delivery" ], "summary": "Create delivery", "description": "Creates a goods receipt with header and lines in one transaction.\nEach line requires `sku`, `amount`, `net`, and `tax` (rate).\n`rebate` defaults to `0`; `transactedAmount` defaults to `amount`.\n", "security": [ { "cookieAuth": [] } ], "x-markdown": "delivery/create.md", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "number", "date", "lines" ], "properties": { "number": { "type": "string" }, "date": { "type": "string", "format": "date-time" }, "supplierId": { "type": "integer", "nullable": true }, "lines": { "type": "array", "items": { "$ref": "#/components/schemas/DeliveryLineInput" } } }, "example": { "number": "DHL-123", "date": "2026-08-10", "supplierId": 1, "lines": [ { "sku": "1", "amount": 10, "net": 99.5, "tax": 19 } ] } } } } }, "responses": { "201": { "description": "Created delivery", "content": { "application/json": { "schema": { "type": "object", "properties": { "delivery": { "$ref": "#/components/schemas/Delivery" } } } } } }, "400": { "description": "Invalid delivery, invalid lines, or no company selected", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Company, supplier, or article not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/delivery/{id}": { "get": { "tags": [ "Delivery" ], "summary": "Get delivery", "description": "Returns a goods receipt with lines for the selected company.", "security": [ { "cookieAuth": [] } ], "x-markdown": "delivery/get.md", "parameters": [ { "in": "path", "name": "id", "required": true, "schema": { "type": "integer" } } ], "responses": { "200": { "description": "Delivery", "content": { "application/json": { "schema": { "type": "object", "properties": { "delivery": { "$ref": "#/components/schemas/Delivery" } } } } } }, "400": { "description": "No company selected or invalid id", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Delivery or company not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "patch": { "tags": [ "Delivery" ], "summary": "Update delivery", "description": "Patches header fields that are present. If `lines` is present, replaces\nall lines; if omitted, leaves lines unchanged. Returns the full delivery.\n", "security": [ { "cookieAuth": [] } ], "x-markdown": "delivery/update.md", "parameters": [ { "in": "path", "name": "id", "required": true, "schema": { "type": "integer" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "number": { "type": "string" }, "date": { "type": "string", "format": "date-time" }, "supplierId": { "type": "integer", "nullable": true }, "lines": { "type": "array", "items": { "$ref": "#/components/schemas/DeliveryLineInput" } } }, "example": { "number": "DHL-124", "lines": [] } } } } }, "responses": { "200": { "description": "Updated delivery", "content": { "application/json": { "schema": { "type": "object", "properties": { "delivery": { "$ref": "#/components/schemas/Delivery" } } } } } }, "400": { "description": "Invalid delivery, invalid lines, invalid id, or no company selected", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Delivery, supplier, article, or company not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "delete": { "tags": [ "Delivery" ], "summary": "Delete delivery", "description": "Deletes the goods receipt (`manifestarticle` rows cascade).\n", "security": [ { "cookieAuth": [] } ], "x-markdown": "delivery/delete.md", "parameters": [ { "in": "path", "name": "id", "required": true, "schema": { "type": "integer" } } ], "responses": { "204": { "description": "Deleted" }, "400": { "description": "No company selected or invalid id", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Delivery not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/customergroup": { "get": { "tags": [ "Customergroup" ], "summary": "List customer groups", "description": "Lists customer groups for the selected company, ordered by `sort`.\nEach item includes `editable`: `false` for implicit `POS` / `Lieferanten`\n(name locked), `true` otherwise. Reorder via `PUT /customergroup/order`.\n", "security": [ { "cookieAuth": [] } ], "x-markdown": "customergroup/list.md", "responses": { "200": { "description": "Customer group list", "content": { "application/json": { "schema": { "type": "object", "properties": { "customergroups": { "type": "array", "items": { "$ref": "#/components/schemas/CustomergroupListItem" } } } } } } }, "400": { "description": "No company selected", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Company not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "post": { "tags": [ "Customergroup" ], "summary": "Create customer group", "description": "Creates a customer group for the selected company.\nAppends with the next `sort` index. Reserved names `POS` and `Lieferanten`\nare not allowed (`Name not allowed`).\n", "security": [ { "cookieAuth": [] } ], "x-markdown": "customergroup/create.md", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "name" ], "properties": { "name": { "type": "string" } }, "example": { "name": "Kunden" } } } } }, "responses": { "201": { "description": "Created customer group", "content": { "application/json": { "schema": { "type": "object", "properties": { "customergroup": { "$ref": "#/components/schemas/Customergroup" } } } } } }, "400": { "description": "Name required, name not allowed, or no company selected", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Company not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/customergroup/order": { "put": { "tags": [ "Customergroup" ], "summary": "Reorder customer groups", "description": "Sets `sort` to each id’s index `0…n-1` in one transaction.\n`customergroupIds` must be exactly the company’s groups (same set, no dups)\nelse `Invalid customer group order`.\n", "security": [ { "cookieAuth": [] } ], "x-markdown": "customergroup/order.md", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "customergroupIds" ], "properties": { "customergroupIds": { "type": "array", "items": { "type": "integer" } } }, "example": { "customergroupIds": [ 4, 3, 1, 2 ] } } } } }, "responses": { "200": { "description": "New order", "content": { "application/json": { "schema": { "type": "object", "properties": { "customergroupIds": { "type": "array", "items": { "type": "integer" } } } } } } }, "400": { "description": "Invalid customer group order or no company selected", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Company not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/customergroup/{id}": { "patch": { "tags": [ "Customergroup" ], "summary": "Update customer group", "description": "Renames a customer group. Implicit `POS` / `Lieferanten` cannot be\nrenamed (`Name cannot be changed`). Reorder via `PUT /customergroup/order`.\n", "security": [ { "cookieAuth": [] } ], "x-markdown": "customergroup/update.md", "parameters": [ { "in": "path", "name": "id", "required": true, "schema": { "type": "integer" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "name" ], "properties": { "name": { "type": "string" } }, "example": { "name": "Stammkunden" } } } } }, "responses": { "200": { "description": "Updated customer group", "content": { "application/json": { "schema": { "type": "object", "properties": { "customergroup": { "$ref": "#/components/schemas/Customergroup" } } } } } }, "400": { "description": "Name required, name cannot be changed, invalid id, or no company selected", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Customer group or company not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "delete": { "tags": [ "Customergroup" ], "summary": "Delete customer group", "description": "Deletes the customer group (cascades entities). Fails with\n`Customer group in use` for the POS group or other NO ACTION references.\n", "security": [ { "cookieAuth": [] } ], "x-markdown": "customergroup/delete.md", "parameters": [ { "in": "path", "name": "id", "required": true, "schema": { "type": "integer" } } ], "responses": { "204": { "description": "Deleted" }, "400": { "description": "No company selected or invalid id", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Customer group not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "Customer group in use", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/company": { "get": { "tags": [ "Company" ], "summary": "List companies", "description": "Lists companies for the authenticated user.", "security": [ { "cookieAuth": [] } ], "x-markdown": "company/list.md", "responses": { "200": { "description": "Company list", "content": { "application/json": { "schema": { "type": "object", "properties": { "companies": { "type": "array", "items": { "$ref": "#/components/schemas/Company" } } } } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "post": { "tags": [ "Company" ], "summary": "Create company", "description": "Creates a company and selects it for the current user.", "security": [ { "cookieAuth": [] } ], "x-markdown": "company/create.md", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "iso639", "name", "taxrate", "reducedtaxrate" ], "properties": { "iso639": { "type": "string" }, "name": { "type": "string" }, "taxrate": { "type": "number" }, "reducedtaxrate": { "type": "number" } }, "example": { "iso639": "de", "name": "Acme GmbH", "taxrate": 19, "reducedtaxrate": 7 } } } } }, "responses": { "201": { "description": "Created; selected company updated", "content": { "application/json": { "schema": { "type": "object", "properties": { "company": { "$ref": "#/components/schemas/Company" }, "user": { "$ref": "#/components/schemas/User" } } } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "Company name not available", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/company/{id}": { "put": { "tags": [ "Company" ], "summary": "Update company", "description": "Updates a company.", "security": [ { "cookieAuth": [] } ], "x-markdown": "company/update.md", "parameters": [ { "in": "path", "name": "id", "required": true, "schema": { "type": "integer" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "iso639", "name", "taxrate", "reducedtaxrate" ], "properties": { "iso639": { "type": "string" }, "name": { "type": "string" }, "taxrate": { "type": "number" }, "reducedtaxrate": { "type": "number" } } } } } }, "responses": { "200": { "description": "Updated", "content": { "application/json": { "schema": { "type": "object", "properties": { "company": { "$ref": "#/components/schemas/Company" } } } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Company not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "Company name not available", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "delete": { "tags": [ "Company" ], "summary": "Delete company", "description": "Deletes a company.", "security": [ { "cookieAuth": [] } ], "x-markdown": "company/delete.md", "parameters": [ { "in": "path", "name": "id", "required": true, "schema": { "type": "integer" } } ], "responses": { "204": { "description": "Deleted" }, "400": { "description": "Invalid id", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/category/tree": { "get": { "tags": [ "Category" ], "summary": "Category tree", "description": "Returns the category tree for the authenticated user's selected company.", "security": [ { "cookieAuth": [] } ], "x-markdown": "category/tree.md", "responses": { "200": { "description": "Tree and articles for the selected company", "content": { "application/json": { "schema": { "type": "object", "properties": { "languageId": { "type": "integer" }, "tree": { "type": "array", "items": { "type": "object", "additionalProperties": true } }, "articles": { "type": "array", "items": { "type": "object", "additionalProperties": true } } } } } } }, "400": { "description": "No company selected", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Company not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/category/export.zip": { "get": { "tags": [ "Category" ], "summary": "Export categories as ZIP", "description": "Builds a `kategorien.zip` for the authenticated user's selected company.\nThe ZIP contains `kategorien.csv` (semicolon-separated, Windows-1252,\ndepth-first in sort order with an `Ebene N` name column per tree depth)\nplus one AVIF entry per category image, named `.avif`\n(content-addressed, so shared hashes are deduplicated).\n", "security": [ { "cookieAuth": [] } ], "responses": { "200": { "description": "ZIP archive", "content": { "application/zip": { "schema": { "type": "string", "format": "binary" } } } }, "400": { "description": "No company selected", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Company not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/category": { "post": { "tags": [ "Category" ], "summary": "Create category", "description": "Creates a category under an authorized parent.", "security": [ { "cookieAuth": [] } ], "x-signals": [ "category" ], "x-markdown": "category/create.md", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "parentId", "name" ], "properties": { "parentId": { "type": "integer" }, "name": { "type": "string" } }, "example": { "parentId": 1, "name": "New category" } } } } }, "responses": { "201": { "description": "Created", "content": { "application/json": { "schema": { "type": "object", "properties": { "category": { "$ref": "#/components/schemas/Category" } } } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Parent not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/category/{id}/articles": { "get": { "tags": [ "Category" ], "summary": "Articles in category", "description": "Returns articles in a category the user is authorized for.", "security": [ { "cookieAuth": [] } ], "x-markdown": "category/articles.md", "parameters": [ { "in": "path", "name": "id", "required": true, "schema": { "type": "integer" }, "description": "Category id" } ], "responses": { "200": { "description": "Articles list", "content": { "application/json": { "schema": { "type": "object", "properties": { "articles": { "type": "array", "items": { "type": "object", "additionalProperties": true } } } } } } }, "400": { "description": "Invalid id", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/category/{id}": { "patch": { "tags": [ "Category" ], "summary": "Update category (rename or move)", "description": "Updates a category the user is authorized for.\n`name` renames the category (re-slugifies in the company default language).\n`parentId` moves it under a new parent (cycle/self → `Invalid parent`).\nEither `name` or `parentId` (or both) is required.\n", "security": [ { "cookieAuth": [] } ], "x-signals": [ "category" ], "x-markdown": "category/update.md", "parameters": [ { "in": "path", "name": "id", "required": true, "schema": { "type": "integer" }, "description": "Category id" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string" }, "parentId": { "type": "integer", "description": "New parent category id" } }, "example": { "parentId": 3 } } } } }, "responses": { "200": { "description": "Updated category (renamed and/or moved)", "content": { "application/json": { "schema": { "type": "object", "properties": { "category": { "type": "object", "required": [ "id", "name", "slug", "parentId", "sort" ], "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "slug": { "type": "string" }, "parentId": { "type": "integer" }, "sort": { "type": "integer" } } } } } } } }, "400": { "description": "Validation error (name or parentId required, or invalid parent)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Category not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "delete": { "tags": [ "Category" ], "summary": "Delete category", "description": "Deletes a category the user is authorized for (not the company root).", "security": [ { "cookieAuth": [] } ], "x-signals": [ "category" ], "x-markdown": "category/delete.md", "parameters": [ { "in": "path", "name": "id", "required": true, "schema": { "type": "integer" }, "description": "Category id" } ], "responses": { "204": { "description": "Deleted" }, "400": { "description": "Invalid id or cannot delete root", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/category/order": { "put": { "tags": [ "Category" ], "summary": "Reorder sibling categories", "description": "Sets the sibling sort order of `categoryIds` under `parentId`.\n`categoryIds` must be exactly the current child set of `parentId`\n(same count, no foreign/duplicate ids). Each child's `sort` is set\nto its index `0..n-1` in one transaction.\n", "security": [ { "cookieAuth": [] } ], "x-signals": [ "category" ], "x-markdown": "category/order.md", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "parentId", "categoryIds" ], "properties": { "parentId": { "type": "integer", "description": "Parent category under which sibling order applies" }, "categoryIds": { "type": "array", "items": { "type": "integer" } } }, "example": { "parentId": 1, "categoryIds": [ 2, 4, 3, 5 ] } } } } }, "responses": { "200": { "description": "New sibling order", "content": { "application/json": { "schema": { "type": "object", "properties": { "categoryIds": { "type": "array", "items": { "type": "integer" } } } } } } }, "400": { "description": "Invalid category order", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Parent category not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/category/{id}/image": { "post": { "tags": [ "Category" ], "summary": "Add category image", "description": "Accepts a full-size AVIF body (`Content-Type: image/avif`).\nFull: stored as-is when already AVIF and ≤1000×1000; otherwise scaled to max 1000×1000.\nThumb always scaled to max 200×200. Stores hash/thumbhash, links `categorypic`.\nAuth like GET /category/tree — user must be authorized for the category's company.\n", "security": [ { "cookieAuth": [] } ], "x-signals": [ "category" ], "x-markdown": "category/add-image.md", "parameters": [ { "in": "path", "name": "id", "required": true, "schema": { "type": "integer" }, "description": "Category id" } ], "requestBody": { "required": true, "content": { "image/avif": { "schema": { "type": "string", "format": "binary" } } } }, "responses": { "201": { "description": "Image stored and linked", "content": { "application/json": { "schema": { "type": "object", "properties": { "category": { "type": "object", "properties": { "id": { "type": "integer" }, "thumbW": { "type": "integer" }, "thumbH": { "type": "integer" }, "thumbHash": { "type": "string" } } } } } } } }, "400": { "description": "Missing body or invalid image", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Category not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "delete": { "tags": [ "Category" ], "summary": "Delete category image", "description": "Removes the category's image. Sets thumbW, thumbH, thumbHash back to null.\nAuth like GET /category/tree — user must be authorized for the category's company.\n", "security": [ { "cookieAuth": [] } ], "x-signals": [ "category" ], "x-markdown": "category/delete-image.md", "parameters": [ { "in": "path", "name": "id", "required": true, "schema": { "type": "integer" }, "description": "Category id" } ], "responses": { "200": { "description": "Image removed", "content": { "application/json": { "schema": { "type": "object", "properties": { "category": { "type": "object", "properties": { "id": { "type": "integer" }, "thumbW": { "type": "integer", "nullable": true }, "thumbH": { "type": "integer", "nullable": true }, "thumbHash": { "type": "string", "nullable": true } } } } } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Category or image not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/brand": { "get": { "tags": [ "Brand" ], "summary": "List brands", "description": "Lists brands for the authenticated user's selected company, sorted by name.", "security": [ { "cookieAuth": [] } ], "x-markdown": "brand/list.md", "responses": { "200": { "description": "Brand list", "content": { "application/json": { "schema": { "type": "object", "properties": { "brands": { "type": "array", "items": { "$ref": "#/components/schemas/Brand" } } } } } } }, "400": { "description": "No company selected", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Company not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "put": { "tags": [ "Brand" ], "summary": "Upsert brand", "description": "Insert or update brand by `name` for the selected company. `gsprpinfo` optional.\n", "security": [ { "cookieAuth": [] } ], "x-markdown": "brand/upsert.md", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "name" ], "properties": { "name": { "type": "string" }, "gsprpinfo": { "type": "string" } }, "example": { "name": "Acme", "gsprpinfo": "Acme GmbH" } } } } }, "responses": { "200": { "description": "Upserted brand", "content": { "application/json": { "schema": { "type": "object", "properties": { "brand": { "$ref": "#/components/schemas/Brand" } } } } } }, "400": { "description": "Invalid brand or no company selected", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Company not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/brand/{id}": { "delete": { "tags": [ "Brand" ], "summary": "Delete brand", "description": "Deletes the brand for the selected company. Fails with `Brand in use`\nif any article still references it.\n", "security": [ { "cookieAuth": [] } ], "x-markdown": "brand/delete.md", "parameters": [ { "in": "path", "name": "id", "required": true, "schema": { "type": "integer" } } ], "responses": { "204": { "description": "Deleted" }, "400": { "description": "No company selected", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Brand not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "Brand in use", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/article": { "post": { "tags": [ "Article" ], "summary": "Create article", "description": "Creates an article under an authorized category (`parentId`).", "security": [ { "cookieAuth": [] } ], "x-signals": [ "articles" ], "x-markdown": "article/create.md", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "parentId", "name" ], "properties": { "parentId": { "type": "integer", "description": "Category id" }, "name": { "type": "string" } }, "example": { "parentId": 10, "name": "New article" } } } } }, "responses": { "201": { "description": "Created", "content": { "application/json": { "schema": { "type": "object", "properties": { "article": { "$ref": "#/components/schemas/ArticleSummary" } } } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized for parent category", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Parent not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/article/export.zip": { "get": { "tags": [ "Article" ], "summary": "Export articles as ZIP", "description": "Builds an `artikel.zip` for the authenticated user's selected company.\n`categoryId` (optional) restricts the export to that category and below;\nit must be reachable via the company's root category. Defaults to the\nwhole company root tree.\nThe ZIP contains `artikel.csv` (semicolon-separated, Windows-1252)\nplus one AVIF entry per article image, named `.avif`\n(content-addressed, so shared hashes are deduplicated).\n", "security": [ { "cookieAuth": [] } ], "parameters": [ { "in": "query", "name": "categoryId", "required": false, "schema": { "type": "integer" }, "description": "Restrict export to this category subtree" } ], "responses": { "200": { "description": "ZIP archive", "content": { "application/zip": { "schema": { "type": "string", "format": "binary" } } } }, "400": { "description": "No company selected", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Company not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/article/{sku}": { "get": { "tags": [ "Article" ], "summary": "Get article by SKU", "description": "Returns article detail by SKU if the user may access its category.", "security": [ { "cookieAuth": [] } ], "x-markdown": "article/get.md", "parameters": [ { "in": "path", "name": "sku", "required": true, "schema": { "type": "string" }, "description": "Article SKU" } ], "responses": { "200": { "description": "Article detail", "content": { "application/json": { "schema": { "type": "object", "properties": { "article": { "$ref": "#/components/schemas/ArticleDetail" } } } } } }, "400": { "description": "Missing sku", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Article not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "patch": { "tags": [ "Article" ], "summary": "Update article by SKU", "description": "Patches article fields. At least one of `name`, `unitamount`, `unitSi` required.\n`unitSi` must already exist for the company. Returns the same detail shape as GET.\n", "security": [ { "cookieAuth": [] } ], "x-signals": [ "articles" ], "x-markdown": "article/update.md", "parameters": [ { "in": "path", "name": "sku", "required": true, "schema": { "type": "string" }, "description": "Article SKU" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string" }, "unitamount": { "type": "number", "description": "Must be finite and `> 0`" }, "unitSi": { "type": "string", "description": "Existing company unit `si`" } }, "example": { "unitamount": 0.24, "unitSi": "l" } } } } }, "responses": { "200": { "description": "Updated article detail", "content": { "application/json": { "schema": { "type": "object", "properties": { "article": { "$ref": "#/components/schemas/ArticleDetail" } } } } } }, "400": { "description": "Validation error (Name required / Invalid unitamount)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Article or unit not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "delete": { "tags": [ "Article" ], "summary": "Delete article by SKU", "description": "Deletes the article when it has no child articles. Cascades related rows;\nnulls `manifestarticle.article_id` and appends a delete comment.\nAuth like GET by SKU.\n", "security": [ { "cookieAuth": [] } ], "x-signals": [ "articles" ], "x-markdown": "article/delete.md", "parameters": [ { "in": "path", "name": "sku", "required": true, "schema": { "type": "string" }, "description": "Article SKU" } ], "responses": { "204": { "description": "Deleted" }, "400": { "description": "Article has children", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Article not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/article/{sku}/history": { "get": { "tags": [ "Article" ], "summary": "Get article history by SKU", "description": "Returns `articlehistory` rows for the article, newest first (`date` DESC, `id` DESC).\nAuth like GET by SKU.\n", "security": [ { "cookieAuth": [] } ], "x-markdown": "article/history.md", "parameters": [ { "in": "path", "name": "sku", "required": true, "schema": { "type": "string" }, "description": "Article SKU" } ], "responses": { "200": { "description": "Article history (newest first)", "content": { "application/json": { "schema": { "type": "object", "required": [ "history" ], "properties": { "history": { "type": "array", "items": { "$ref": "#/components/schemas/ArticleHistoryEntry" } } }, "example": { "history": [ { "id": 42, "date": "2026-08-05T07:15:00.000Z", "text": "Name geändert von \"Tee\" auf \"Grüner Tee\"\nEinheit geändert von \"500g\" auf \"1kg\"" }, { "id": 41, "date": "2026-08-01T09:00:00.000Z", "text": "Name geändert von \"\" auf \"Tee\"" } ] } } } } }, "400": { "description": "Missing sku", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Article not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/article/{sku}/prices": { "put": { "tags": [ "Article" ], "summary": "Replace POS prices by SKU", "description": "Replace-all prices for the company POS customer group.\nEach entry: `price` (net), `tax` (rate, ensured via tax.ensure), optional `minmultiplier` (default 1).\nEmpty `prices` clears all POS prices. Returns the same shape as GET by SKU.\n", "security": [ { "cookieAuth": [] } ], "x-signals": [ "article-" ], "x-markdown": "article/set-prices.md", "parameters": [ { "in": "path", "name": "sku", "required": true, "schema": { "type": "string" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "prices" ], "properties": { "prices": { "type": "array", "items": { "type": "object", "required": [ "price", "tax" ], "properties": { "price": { "type": "number", "description": "Net price" }, "tax": { "type": "number", "description": "Tax rate (e.g. 19)" }, "minmultiplier": { "type": "number", "description": "Defaults to 1; must be unique per request" } } } } }, "example": { "prices": [ { "price": 10.5, "tax": 19 }, { "price": 9.9, "tax": 19, "minmultiplier": 10 } ] } } } } }, "responses": { "200": { "description": "Updated article detail", "content": { "application/json": { "schema": { "type": "object", "properties": { "article": { "$ref": "#/components/schemas/ArticleDetail" } } } } } }, "400": { "description": "Invalid prices or POS customer group required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Article not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/article/{sku}/image": { "post": { "tags": [ "Article" ], "summary": "Add article image", "description": "Accepts a full-size AVIF body (`Content-Type: image/avif`).\nFull: stored as-is when already AVIF and ≤1000×1000; otherwise scaled to max 1000×1000.\nThumb always scaled to max 200×200. Stores hash/thumbhash, links `articlepic`.\nAuth like GET by SKU.\n", "security": [ { "cookieAuth": [] } ], "x-signals": [ "article-" ], "x-markdown": "article/add-image.md", "parameters": [ { "in": "path", "name": "sku", "required": true, "schema": { "type": "string" }, "description": "Article SKU" } ], "requestBody": { "required": true, "content": { "image/avif": { "schema": { "type": "string", "format": "binary" } } } }, "responses": { "201": { "description": "Image stored and linked", "content": { "application/json": { "schema": { "type": "object", "properties": { "image": { "$ref": "#/components/schemas/ArticleImage" } } } } } }, "400": { "description": "Missing body or invalid image", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Article not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/article/{sku}/image/{fullHash}": { "delete": { "tags": [ "Article" ], "summary": "Delete article image", "description": "Removes `articlepic` and the `pic` row for this article (`fullHash`). Auth like GET by SKU.", "security": [ { "cookieAuth": [] } ], "x-signals": [ "article-" ], "x-markdown": "article/delete-image.md", "parameters": [ { "in": "path", "name": "sku", "required": true, "schema": { "type": "string" } }, { "in": "path", "name": "fullHash", "required": true, "schema": { "type": "string", "pattern": "^[a-fA-F0-9]{64}$" } } ], "responses": { "204": { "description": "Deleted" }, "400": { "description": "Invalid hash", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Article or image not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/article/{sku}/images/order": { "put": { "tags": [ "Article" ], "summary": "Reorder article images", "description": "Sets `articlepic.sort` to `0..n-1` in the given order.\n`fullHashes` must list every image of the article exactly once.\n", "security": [ { "cookieAuth": [] } ], "x-signals": [ "article-" ], "x-markdown": "article/order-images.md", "parameters": [ { "in": "path", "name": "sku", "required": true, "schema": { "type": "string" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "fullHashes" ], "properties": { "fullHashes": { "type": "array", "items": { "type": "string", "pattern": "^[a-fA-F0-9]{64}$" } } }, "example": { "fullHashes": [ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" ] } } } } }, "responses": { "200": { "description": "New order", "content": { "application/json": { "schema": { "type": "object", "properties": { "fullHashes": { "type": "array", "items": { "type": "string" } } } } } } }, "400": { "description": "Invalid image order", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Article not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/article/{sku}/eans": { "put": { "tags": [ "Article" ], "summary": "Replace article EANs by SKU", "description": "Replace-all EANs for the article. EANs must be non-empty unique strings;\nempty array clears all EANs. Returns the same shape as GET by SKU.\n", "security": [ { "cookieAuth": [] } ], "x-signals": [ "article-" ], "x-markdown": "article/set-eans.md", "parameters": [ { "in": "path", "name": "sku", "required": true, "schema": { "type": "string" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "eans" ], "properties": { "eans": { "type": "array", "items": { "type": "string" } } }, "example": { "eans": [ "4001234567890" ] } } } } }, "responses": { "200": { "description": "Updated article detail", "content": { "application/json": { "schema": { "type": "object", "properties": { "article": { "$ref": "#/components/schemas/ArticleDetail" } } } } } }, "400": { "description": "Invalid eans", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Article not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/article/{sku}/brand": { "put": { "tags": [ "Article" ], "summary": "Set or clear article brand by SKU", "description": "Sets the article's `brandId` (must belong to the article's company).\nPass `null` to clear the brand. Returns the same shape as GET by SKU.\n", "security": [ { "cookieAuth": [] } ], "x-signals": [ "article-" ], "x-markdown": "article/set-brand.md", "parameters": [ { "in": "path", "name": "sku", "required": true, "schema": { "type": "string" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "brandId" ], "properties": { "brandId": { "type": "integer", "nullable": true } }, "example": { "brandId": 3 } } } } }, "responses": { "200": { "description": "Updated article detail", "content": { "application/json": { "schema": { "type": "object", "properties": { "article": { "$ref": "#/components/schemas/ArticleDetail" } } } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Article or brand not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/article/{sku}/weight": { "put": { "tags": [ "Article" ], "summary": "Set article weight by SKU", "description": "Sets the article's `weight`. Must be finite and `>= 0`.\nReturns the same shape as GET by SKU.\n", "security": [ { "cookieAuth": [] } ], "x-signals": [ "article-" ], "x-markdown": "article/set-weight.md", "parameters": [ { "in": "path", "name": "sku", "required": true, "schema": { "type": "string" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "weight" ], "properties": { "weight": { "type": "number" } }, "example": { "weight": 1.5 } } } } }, "responses": { "200": { "description": "Updated article detail", "content": { "application/json": { "schema": { "type": "object", "properties": { "article": { "$ref": "#/components/schemas/ArticleDetail" } } } } } }, "400": { "description": "Invalid weight", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Article not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/article/{sku}/categories": { "put": { "tags": [ "Article" ], "summary": "Replace article categories by SKU", "description": "Replace-all the article's category list. Each category must belong to\nthe article's company; ids must be unique. Empty array removes the\narticle from all categories. Returns the same shape as GET by SKU.\n", "security": [ { "cookieAuth": [] } ], "x-signals": [ "article-" ], "x-markdown": "article/set-categories.md", "parameters": [ { "in": "path", "name": "sku", "required": true, "schema": { "type": "string" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "categoryIds" ], "properties": { "categoryIds": { "type": "array", "items": { "type": "integer" } } }, "example": { "categoryIds": [ 4, 7 ] } } } } }, "responses": { "200": { "description": "Updated article detail", "content": { "application/json": { "schema": { "type": "object", "properties": { "article": { "$ref": "#/components/schemas/ArticleDetail" } } } } } }, "400": { "description": "Invalid categoryIds", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Article not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/articleIndex": { "get": { "tags": [ "Article" ], "summary": "List article search index", "description": "Returns every article reachable via the company's category tree.\nDesigned for client-side search / autocomplete preload.\nAuth via company access.\n", "security": [ { "cookieAuth": [] } ], "responses": { "200": { "description": "Search index", "content": { "application/json": { "schema": { "type": "object", "properties": { "articles": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "sku": { "type": "string" }, "name": { "type": "string", "nullable": true }, "eans": { "type": "array", "items": { "type": "string" } }, "categoryIds": { "type": "array", "items": { "type": "integer" } } } } } } } } } }, "400": { "description": "No company selected", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Not authorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Company not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/auth/selected-company": { "put": { "tags": [ "Auth" ], "summary": "Set selected company", "description": "Sets the authenticated user's selected company.", "security": [ { "cookieAuth": [] } ], "x-markdown": "auth/selected-company.md", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "companyId" ], "properties": { "companyId": { "type": "integer", "minimum": 1 } }, "example": { "companyId": 1 } } } } }, "responses": { "200": { "description": "Updated user", "content": { "application/json": { "schema": { "type": "object", "properties": { "user": { "$ref": "#/components/schemas/User" } } } } } }, "400": { "description": "Invalid companyId", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/auth/me": { "get": { "tags": [ "Auth" ], "summary": "Current user", "description": "Returns the authenticated user from the session cookie and the Google client ID.\n`user.picture` is the stored profile image as a base64 string when present.\nUnauthenticated calls still receive `googleClientId` with 401.\n", "security": [ { "cookieAuth": [] } ], "x-markdown": "auth/me.md", "responses": { "200": { "description": "Authenticated", "content": { "application/json": { "schema": { "type": "object", "properties": { "user": { "$ref": "#/components/schemas/User" }, "googleClientId": { "type": "string" } } } } } }, "401": { "description": "Unauthorized — still includes `googleClientId` for the frontend", "content": { "application/json": { "schema": { "type": "object", "properties": { "error": { "type": "string" }, "googleClientId": { "type": "string" } } } } } } } } }, "/auth/logout": { "post": { "tags": [ "Auth" ], "summary": "Logout", "description": "Clears the HTTP-only `token` cookie set at login.", "security": [], "x-markdown": "auth/logout.md", "responses": { "200": { "description": "Logged out", "content": { "application/json": { "schema": { "type": "object", "properties": { "message": { "type": "string", "example": "Logged out successfully" } } } } } } } } }, "/auth/login": { "post": { "tags": [ "Auth" ], "summary": "Login with Google ID token", "description": "Verifies a Google ID token, checks allowlist (or creates entry when `IMPLICIT_CREATE=true`),\nstores Google `sub` and profile image bytes, and sets an HTTP-only `token` cookie.\n`user.picture` is the profile image as base64 (same as GET /auth/me).\n", "security": [], "x-markdown": "auth/login.md", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "idToken" ], "properties": { "idToken": { "type": "string", "description": "Google ID token from Google Identity Services" } }, "example": { "idToken": "" } } } } }, "responses": { "200": { "description": "Login successful; `token` cookie set", "content": { "application/json": { "schema": { "type": "object", "properties": { "message": { "type": "string", "example": "Login successful" }, "user": { "$ref": "#/components/schemas/User" } } } } } }, "400": { "description": "Missing token or Google client not configured", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Invalid Google token or account", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Email not on allowlist", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "components": { "securitySchemes": { "cookieAuth": { "type": "apiKey", "in": "cookie", "name": "token", "description": "HTTP-only session cookie set by POST /auth/login" } }, "schemas": { "Error": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string" } } }, "User": { "type": "object", "required": [ "id", "email", "selectedCompanyId" ], "properties": { "id": { "type": "integer" }, "email": { "type": "string", "format": "email" }, "selectedCompanyId": { "type": "integer", "nullable": true }, "picture": { "type": "string", "description": "Profile image as base64 (from stored picturebinary); omitted when unset" } } }, "Company": { "type": "object", "properties": { "id": { "type": "integer" }, "iso639": { "type": "string" }, "name": { "type": "string" }, "taxrate": { "type": "number" }, "reducedtaxrate": { "type": "number" } } }, "Category": { "type": "object", "properties": { "id": { "type": "integer" }, "parentId": { "type": "integer", "nullable": true }, "sort": { "type": "integer" }, "name": { "type": "string" } } }, "Unit": { "type": "object", "required": [ "si", "basesi", "basesifactor" ], "properties": { "si": { "type": "string" }, "basesi": { "type": "string" }, "basesifactor": { "type": "number" }, "symbol": { "type": "string", "nullable": true }, "name": { "type": "string", "nullable": true } } }, "ArticleSummary": { "type": "object", "properties": { "id": { "type": "integer" }, "parentId": { "type": "integer" }, "name": { "type": "string" }, "sku": { "type": "string" } } }, "ArticleDetail": { "type": "object", "description": "Full article payload (unit, brand, categoryIds, photos, eans, prices, …)", "additionalProperties": true }, "ArticleHistoryEntry": { "type": "object", "required": [ "id", "date", "text" ], "properties": { "id": { "type": "integer" }, "date": { "type": "string", "format": "date-time" }, "text": { "type": "string" } } }, "Brand": { "type": "object", "required": [ "id", "name" ], "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "gsprpinfo": { "type": "string", "nullable": true } } }, "Supplier": { "type": "object", "required": [ "id", "name", "createdAt", "updatedAt" ], "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" } } }, "Customergroup": { "type": "object", "required": [ "id", "name", "sort" ], "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "sort": { "type": "integer" } } }, "CustomergroupListItem": { "allOf": [ { "$ref": "#/components/schemas/Customergroup" }, { "type": "object", "required": [ "editable" ], "properties": { "editable": { "type": "boolean", "description": "`false` for implicit groups `POS` and `Lieferanten` (name locked).\n`true` for user-created groups. Reorder all groups via\n`PUT /customergroup/order`.\n" } } } ] }, "Entity": { "type": "object", "required": [ "id", "customergroupId", "firstName", "name", "createdAt", "updatedAt" ], "properties": { "id": { "type": "integer" }, "customergroupId": { "type": "integer" }, "firstName": { "type": "string", "nullable": true }, "name": { "type": "string" }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" } } }, "EntityAddress": { "type": "object", "required": [ "id", "entityId", "addressType" ], "properties": { "id": { "type": "integer" }, "entityId": { "type": "integer" }, "firstName": { "type": "string", "nullable": true }, "name": { "type": "string", "nullable": true }, "street": { "type": "string", "nullable": true }, "streetNumber": { "type": "string", "nullable": true }, "zip": { "type": "string", "nullable": true }, "city": { "type": "string", "nullable": true }, "county": { "type": "string", "nullable": true }, "addressType": { "type": "string" } } }, "EntityContact": { "type": "object", "required": [ "id", "entityId", "contactType", "contact" ], "properties": { "id": { "type": "integer" }, "entityId": { "type": "integer" }, "contactType": { "type": "string", "description": "e.g. email, phone" }, "contact": { "type": "string" } } }, "DeliverySummary": { "type": "object", "required": [ "id", "number", "type", "supplierId", "date" ], "properties": { "id": { "type": "integer" }, "number": { "type": "string" }, "type": { "type": "string", "enum": [ "delivery" ] }, "supplierId": { "type": "integer", "nullable": true, "description": "manifest.entity_id" }, "date": { "type": "string", "format": "date-time" } } }, "DeliveryLine": { "type": "object", "required": [ "id", "articleId", "sku", "amount", "net", "transactedAmount", "rebate", "tax", "comment" ], "properties": { "id": { "type": "integer" }, "articleId": { "type": "integer", "nullable": true }, "sku": { "type": "string", "nullable": true }, "amount": { "type": "number" }, "net": { "type": "number" }, "transactedAmount": { "type": "number" }, "rebate": { "type": "number" }, "tax": { "type": "number", "description": "tax.rate" }, "comment": { "type": "string", "nullable": true } } }, "DeliveryLineInput": { "type": "object", "required": [ "sku", "amount", "net", "tax" ], "properties": { "sku": { "type": "string" }, "amount": { "type": "number" }, "net": { "type": "number" }, "tax": { "type": "number", "description": "Tax rate (resolved via tax.ensure)" }, "rebate": { "type": "number", "description": "Defaults to 0" }, "transactedAmount": { "type": "number", "description": "Defaults to amount" }, "comment": { "type": "string", "nullable": true } } }, "Delivery": { "allOf": [ { "$ref": "#/components/schemas/DeliverySummary" }, { "type": "object", "required": [ "lines" ], "properties": { "lines": { "type": "array", "items": { "$ref": "#/components/schemas/DeliveryLine" } } } } ] }, "ManifestLine": { "type": "object", "required": [ "id", "articleId", "sku", "amount", "net", "rebate", "tax", "comment" ], "description": "Line shape for quote and order (no transactedAmount)", "properties": { "id": { "type": "integer" }, "articleId": { "type": "integer", "nullable": true }, "sku": { "type": "string", "nullable": true }, "amount": { "type": "number" }, "net": { "type": "number" }, "rebate": { "type": "number" }, "tax": { "type": "number", "description": "tax.rate" }, "comment": { "type": "string", "nullable": true } } }, "ManifestLineInput": { "type": "object", "required": [ "sku", "amount", "net", "tax" ], "properties": { "sku": { "type": "string" }, "amount": { "type": "number" }, "net": { "type": "number" }, "tax": { "type": "number", "description": "Tax rate (resolved via tax.ensure)" }, "rebate": { "type": "number", "description": "Defaults to 0" }, "comment": { "type": "string", "nullable": true } } }, "QuoteSummary": { "type": "object", "required": [ "id", "number", "type", "entityId", "date" ], "properties": { "id": { "type": "integer" }, "number": { "type": "string" }, "type": { "type": "string", "enum": [ "quote" ] }, "entityId": { "type": "integer", "nullable": true, "description": "Customer (manifest.entity_id)" }, "date": { "type": "string", "format": "date-time", "description": "Document date / valid-until" } } }, "Quote": { "allOf": [ { "$ref": "#/components/schemas/QuoteSummary" }, { "type": "object", "required": [ "lines" ], "properties": { "lines": { "type": "array", "items": { "$ref": "#/components/schemas/ManifestLine" } } } } ] }, "OrderSummary": { "type": "object", "required": [ "id", "number", "type", "entityId", "date" ], "properties": { "id": { "type": "integer" }, "number": { "type": "string" }, "type": { "type": "string", "enum": [ "order" ] }, "entityId": { "type": "integer", "nullable": true, "description": "Customer (manifest.entity_id)" }, "date": { "type": "string", "format": "date-time", "description": "Document date / expected delivery" } } }, "Order": { "allOf": [ { "$ref": "#/components/schemas/OrderSummary" }, { "type": "object", "required": [ "lines" ], "properties": { "lines": { "type": "array", "items": { "$ref": "#/components/schemas/ManifestLine" } } } } ] }, "ArticleImage": { "type": "object", "required": [ "picId", "sort", "full", "thumb", "hash", "thumbHash" ], "properties": { "picId": { "type": "integer" }, "sort": { "type": "integer" }, "full": { "type": "object", "required": [ "w", "h", "size" ], "properties": { "w": { "type": "integer" }, "h": { "type": "integer" }, "size": { "type": "integer", "description": "Byte length of full AVIF (as-is when already AVIF ≤1000×1000)" } } }, "thumb": { "type": "object", "required": [ "w", "h" ], "properties": { "w": { "type": "integer" }, "h": { "type": "integer" } } }, "hash": { "type": "string", "description": "SHA-256 hex of full image bytes" }, "thumbHash": { "type": "string", "description": "Base64 thumbhash of thumb preview" } } } } } }