Files
dashback/docs/api/openapi.json
seb c6cb1bc229 feat: add article search index endpoint and category export.zip
- Add GET /api/articleIndex returning the article search index (listSearchIndex)
- Add GET /api/category/export.zip returning a kategorien.zip with a
  Windows-1252 CSV (depth-first, Ebene N columns) plus one AVIF per category image
  (content-addressed, deduplicated) using jszip
2026-08-04 00:30:32 +02:00

4328 lines
114 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"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": "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": "Image",
"description": "Content-addressed AVIF by fullHash"
}
],
"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"
}
}
}
}
}
}
},
"/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"
}
}
}
}
}
}
},
"/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"
}
}
}
}
}
}
}
}
},
"/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 `<fullHash>.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-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-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-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-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-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-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-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/{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-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-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}/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-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-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-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-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-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-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-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-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. Unauthenticated calls still receive `googleClientId` with 401.",
"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`), and sets an HTTP-only `token` cookie.",
"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": "<google-id-token>"
}
}
}
}
},
"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",
"format": "uri"
}
}
},
"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
},
"Brand": {
"type": "object",
"required": [
"id",
"name"
],
"properties": {
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"gsprpinfo": {
"type": "string",
"nullable": true
}
}
},
"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"
}
}
}
}
}
}